情况
我正在构建一个 C# Web 应用程序,我想将我的应用程序配置建模为通过服务的构造函数提交的显式依赖项,而不是直接依赖于每个类中的System.Configuration.ConfigurationManager
。这在过去确实经常咬我,所以我希望依赖关系是明确的,以便项目的下一个维护者(可能是未来的我)不必猜测我的服务从哪里获得他们的配置设置 - 最重要的是它对 TDD 更友好。此外,我目前正在阅读Eric Evan的领域驱动设计,我真的很想接受他的DDD方法。
我开始对配置类和相应的值对象进行建模以避免原始痴迷,但我在途中遇到了一些颠簸,我不确定如何适当地处理它们。这是我目前的方法:
// Role interface that can be requested via constructor injection
interface IAppConnectionStringsConfig
{
OleDbConnectionString AuthenticationConnectionString { get; }
}
// A base class for handling common functionality like
// parsing comma separated lists or default values
class abstract AppConfigBase
{
protected string GetStringAppSetting(string key)
{
// Get the appropriate string or a default value from
// System.Configuration.ConfigurationManager
return theSettingFromSomeConfigSource;
}
}
// A value object for OLEDB connection strings that also has a
// convenient implicit conversion to string
class OleDbConnectionString
{
public readonly string Value;
public OleDbConnectionString(string connectionString)
{
Contract.Requires(connectionString != null);
this.VerifyStructure(connectionString);
this.Value = connectionString;
}
private void VerifyStructure(string text)
{
Contract.Requires(text != null);
// Verify that the given string fulfills the special
// needs of an OleDbConnectionString (including Provider=...)
if (!/* isValidOleDbConnectionString */)
{
throw new FormatException();
}
}
public implicit operator string(ConnectionString conn)
{
return conn.Value;
}
}
// The actual app config that implements our role interface
class AppConfig : AppConfigBase, IAppConnectionStringsConfig
{
public OleDbConnectionString AuthenticationConnectionString
{
get
{
return new OleDbConnectionString(this.GetStringAppSetting("authconn"));
}
}
}
问题所在
我知道构造函数逻辑应该是最小的,从构造函数调用虚拟方法不是一个好主意。我的问题如下:
- 1) 我应该把
OleDbConnectionString
的验证逻辑放在哪里?我真的很想防止在无效状态下创建值对象 - 这在日常工作中非常有用:-)- 我有一种感觉,这是应该由类本身拥有的域逻辑,但另一方面,构造函数应该尽可能少地做 - 字符串解析会不会太多还是可以?
- 我可以创建一个验证器,但我肯定必须通过构造函数将其交出以便能够正确测试该东西,然后我必须手动连接或使用工厂(我绝对不使用服务定位器)。最重要的是,现在的验证将隐藏在单独的服务中;我不会有时间耦合,因为构造函数需要验证器,但这看起来仍然不正确。
- 2)我想知道将DDD值对象
structs
是否合适?顾名思义,它们代表单个值,并且该值是不可变的。但它们将包含验证形式的业务逻辑 - 3) 是否可以使用属性来检索连接字符串?如果字符串的格式无效,则可能会引发异常。此外,实现完全有可能从从 xml 配置文件读取更改为查询数据库。
- 4)欢迎对设计提出任何其他意见!
作为旁注,我已经在使用代码协定,并且有一种方法可以指定对象不变量,但我不知道这是否真的是一个好主意,因为这些合约是选择加入的,并且在它们处于非活动状态的情况下,不变量不再受到主动保护。我不确定这一点,出于开发目的,尽早发现错误可能没问题,但对于生产来说,这似乎不对劲。
感谢!
我从未真正将常规设置视为 DDD 问题 - 您是针对设置及其保存方式对域进行建模,还是仅允许在具有一些内部部分建模为 DDD 的应用程序中保存和使用设置?
您可以通过将获取设置的关注点与使用这些设置的内容分开来拆分它。
是否可以使用属性来检索连接字符串?如果字符串的格式无效,则可能会引发异常。
我认为如果无法检索设置,抛出异常不是一个好主意,这样您就可以返回允许程序继续的默认值。
但也要记住,默认返回值(即密码或网络地址)可能会导致依赖于该设置的内容引发异常。
我会考虑允许施工正常进行,但是当使用该服务时,即 Sender.Send()
或Sender.Connect()
是引发异常的时间。
我应该将 OleDbConnectionString 的验证逻辑放在哪里?我真的很想防止在无效状态下创建值对象
我创建的对象永远不会返回无效结果,但它们确实返回默认设置值:
public class ApplicationSettings : IIdentityAppSettings, IEventStoreSettings
{
/* snip */
static readonly object KeyLock = new object();
public byte[] StsSigningKey
{
get
{
byte[] key = null;
lock (KeyLock)
{
var configManager = WebConfigurationManager.OpenWebConfiguration("/");
var configElement = configManager.AppSettings.Settings["StsSigningKey"];
if (configElement == null)
{
key = CryptoRandom.CreateRandomKey(32);
configManager.AppSettings.Settings.Add("StsSigningKey", Convert.ToBase64String(key));
configManager.Save(ConfigurationSaveMode.Modified); // save to config file
}
else
{
key = Convert.FromBase64String(configElement.Value);
}
}
return key;
}
/* snip */
}
}
我通常做什么
我为域模型中定义的每个边界上下文提供了设置接口,作为基础结构的一部分 - 这允许我可以引用和信任的许多已知接口提供某种形式的设置。
ApplicationSettings
是在托管我的边界上下文的代码中定义的,无论是控制台应用、WebAPI 还是 MVC 等,我可能在同一进程下托管多个边界上下文,或者可能将它们拆分为单独的进程,无论哪种方式,托管应用程序的工作都是提供相关的应用程序设置,并且可以通过 IoC 容器完成连接。
public class ApplicationSettings : IIdentityAppSettings, IEventStoreSettings
{
// implement interfaces here
}
public interface IEventStoreSettings
{
string EventStoreUsername { get; }
string EventStorePassword { get; }
string EventStoreAddress { get; }
int EventStorePort { get; }
}
public interface IIdentityAppSettings
{
byte[] StsSigningKey { get; }
}
我使用 SimpleInjector .NET IoC 容器来连接我的应用程序。然后,我使用 SimpleInjector 注册所有应用程序接口(因此我可以基于任何应用程序接口进行查询并返回设置类对象):
resolver.RegisterAsImplementedInterfaces<ApplicationSettings>();
然后我可以注入特定的接口,一个例子是使用IRepository的命令处理程序,而EventStoreRepository(作为IRepository的实现连接)使用IEventStoreSettings(作为ApplicationSettings实例连接
):public class HandleUserStats : ICommandHandler<UserStats>
{
protected IRepository repository;
public HandleUserStats(IRepository repository)
{
this.repository = repository;
}
public void Handle(UserStats stats)
{
// do something
}
}
我的存储库反过来会被连接起来:
public class EventStoreRepository : IRepository
{
IEventStoreSettings eventStoreSettings;
public EventStoreRepository(IEventStoreSettings eventStoreSettings)
{
this.eventStoreSettings = eventStoreSettings;
}
public void Write(object obj)
{
// just some mockup code to show how to access setting
var eventStoreClient = new EventStoreClient(
this.eventStoreSettings.EventStoreUsername,
this.eventStoreSettings.EventStorePassword,
this.eventStoreSettings.EventStoreAddress,
this.eventStoreSettings.Port
);
// if ever there was an exception either during setup of the connection, or
// exception (if you don't return a default value) accessing settings, it
// could be caught and bubbled up as an InfrastructureException
// now do something with the event store! ....
}
}
我允许从某些外部源(如 WCF 接收或 MVC 控制器操作)传入设置,并通过获取resolver.GetInstance<CommandHandler<UserStats>>();
来连接,从而将我的所有设置一直连接到实现级别。