我们在《 聊聊默认支持的各种配置源 》和《 深入了解三种针对文件(JSON、XML与INI)的配置源 》对配置模型中默认提供的各种ConfigurationSource进行了深入详尽的介绍,如果它们依然不能满足项目中的配置需求,我们可以还可以通过自定义ConfigurationProvider来支持我们希望的配置来源。就配置数据的持久化方式来说,将培植存储在数据库中应该是一种非常常见的方式,接下来我们就是创建一个针对数据库的ConfigurationSource,它采用最新的Entity Framework Core来完成数据库的存取操作。篇幅所限,我们不可能对Entity Framework Core相关的编程作单独介绍,如果读者朋友们对此不太熟悉,可以查阅Entity Framework Core在线文档。 [ 本文已经同步到《ASP.NET Core框架揭秘》之中]
目录
一、在应用中使用自定义的DbConfigurationSource
二、ApplicationSetting & ApplicationSettingsContext
三、DbConfigurationSource
四、DbConfigurationProvider
五、扩展方法AddDatabase
我们将这个自定义ConfigurationSource命名为DbConfigurationSource。在正式对它的实现展开介绍之前,我们先来看看它在项目中的应用。我们创建一个控制台程序来演示对这个DbConfigurationSource应用。我们将配置保存在SQL Server数据库中的某个数据表中,并采用Entity Framework Core来读取配置,所以我们需要添加针对“ Microsoft.EntityFrameworkCore”和“Microsoft.EntityFrameworkCore.SqlServer”这两个NuGet包的依赖。除此之外,我们的实例程序会采用Options模式将读取的配置绑定为了一个Options对象,所以我们添加了针对NuGet包“Microsoft.Extensions.DependencyInjection”和“Microsoft.Extensions.Options.ConfigurationExtensions”的依赖。
{ ... "buildOptions": { ... "copyToOutput": "connectionString.json" }, "dependencies": { ... "Microsoft.Extensions.Options.ConfigurationExtensions" : "1.0.0", "Microsoft.Extensions.DependencyInjection" : "1.0.0", "Microsoft.Extensions.Configuration.Json" : "1.0.0", "Microsoft.EntityFrameworkCore.SqlServer" : "1.0.0", "Microsoft.EntityFrameworkCore" : "1.0.0" } }
我们将链接字符串作为配置定义在一个名为“connectionString.json”的JSON文件中,所以我们添加了针对NuGet包“Microsoft.Extensions.Configuration.Json”的依赖。链接字符串采用如下的形式定义在这个JSON文件中的定义,我们修改了“buildOptions/copyToOutput”配置项使这个文件可以在编译的时候可以自动拷贝到输出目录下。
{ "connectionStrings": { "defaultDb": "Server = ... ; Database=...; Uid = ...; Pwd = ..." } }
我们编写了如下的程序来演示针对自定义ConfigurationSource(DbConfigurationSource)的应用。我们首先创建了一个ConfigurationBuilder对象,并注册了一个指向“connectionString.json”文件的JsonConfigurationSource。针对DbConfigurationSource的注册体现在扩展方法AddDatabase上,这个方法接收两个参数,它们分别代表链接字符串的名称和初始的配置数据。前者正式“connectionString.json”设置的连接字符串名称“defaultDb”,后者是一个字典对象,它提供的原始配置正好可以构成一个Profile对象。在利用ConfigurationBuilder创建出相应的Configuration对象之后,我们采用标准的Options编程模式读取配置将将其绑定为一个Profile对象。
var initialSettings = new Dictionary<string, string> { ["Gender"] = "Male", ["Age"] = "18", ["ContactInfo:EmailAddress"] = "foobar@outlook.com", ["ContactInfo:PhoneNo"] = "123456789" }; IConfiguration config = new ConfigurationBuilder() .AddJsonFile("connectionString.json") .AddDatabase("DefaultDb", initialSettings) .Build(); Profile profile = new ServiceCollection() .AddOptions() .Configure<Profile>(config) .BuildServiceProvider() .GetService<IOptions<Profile>>() .Value; Debug.Assert(profile.Gender == Gender.Male); Debug.Assert(profile.Age == 18); Debug.Assert(profile.ContactInfo.EmailAddress == "foobar@outlook.com"); Debug.Assert(profile.ContactInfo.PhoneNo == "123456789"); public class Profile { public Gender Gender { get; set; } public int Age { get; set; } public ContactInfo ContactInfo { get; set; } } public class ContactInfo { public string EmailAddress { get; set; } public string PhoneNo { get; set; } } public enum Gender { Male, Female }
如上面的代码片断所示,针对DbConfigurationSource的应用仅仅体现在我们为ConfigurationBuilder定义的扩展方法AddDatabase上,所以使用起来是非常方便的,那么这个扩展方法背后有着怎样的逻辑实现呢?DbConfigurationSource采用Entity Framework Core以Code First的方式进行数据操作,如下所示的ApplicationSetting是表示基本配置项的POCO类型,我们将配置项的Key以小写的方式存储。另一个ApplicationSettingsContext是对应的DbContext类型。
[Table("ApplicationSettings")] public class ApplicationSetting { private string key; [Key] public string Key { get { return key; } set { key = value.ToLowerInvariant(); } } [Required] [MaxLength(512)] public string Value { get; set; } public ApplicationSetting() {} public ApplicationSetting(string key, string value) { this.Key = key; this.Value = value; } } public class ApplicationSettingsContext : DbContext { public ApplicationSettingsContext(DbContextOptions options) : base(options) {} public DbSet<ApplicationSetting> Settings { get; set; } }
如下所示的是DbConfigurationSource的定义,它的构造函数接受两个参数,第一个参数类型为Action<DbContextOptionsBuilder>的委托对象,我们用它来对创建DbContext采用的DbContextOptions进行设置,另一个可选的参数用来指定一些需要自动初始化的配置项。DbConfigurationSource在重写的Build方法中利用这两个对象创建一个DbConfigurationProvider对象。
public class DbConfigurationSource : IConfigurationSource { private Action<DbContextOptionsBuilder> _setup; private IDictionary<string, string> _initialSettings; public DbConfigurationSource(Action<DbContextOptionsBuilder> setup, IDictionary<string, string> initialSettings = null) { _setup = setup; _initialSettings = initialSettings; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new DbConfigurationProvider(_setup, _initialSettings); } }
DbConfigurationProvider派生于抽象类ConfigurationProvider。在重写的Load方法中,它会根据提供的Action<DbContextOptionsBuilder>创建ApplicationSettingsContext对象,并利用后者从数据库中读取配置数据并转换成字典对象并赋值给代表配置字典的Data属性。如果数据表中没有数据,该方法还会利用这个DbContext对象将提供的初始化配置添加到数据库中。
public class DbConfigurationProvider: ConfigurationProvider { private IDictionary<string, string> _initialSettings; private Action<DbContextOptionsBuilder> _setup; public DbConfigurationProvider(Action<DbContextOptionsBuilder> setup, IDictionary<string, string> initialSettings) { _setup = setup; _initialSettings = initialSettings?? new Dictionary<string, string>() ; } public override void Load() { DbContextOptionsBuilder<ApplicationSettingsContext> builder = new DbContextOptionsBuilder<ApplicationSettingsContext>(); _setup(builder); using (ApplicationSettingsContext dbContext = new ApplicationSettingsContext(builder.Options)) { dbContext.Database.EnsureCreated(); this.Data = dbContext.Settings.Any()? dbContext.Settings.ToDictionary(it => it.Key, it => it.Value, StringComparer.OrdinalIgnoreCase): this.Initialize(dbContext); } } private IDictionary<string, string> Initialize(ApplicationSettingsContext dbContext) { foreach (var item in _initialSettings) { dbContext.Settings.Add(new ApplicationSetting(item.Key, item.Value)); } return _initialSettings.ToDictionary(it => it.Key, it => it.Value, StringComparer.OrdinalIgnoreCase); } }
实例演示中用来注册DbConfigurationSource的扩展方法AddDatabase具有如下的定义。该方法首先调用ConfigurationBuilder的Build方法创建出一个Configuration对象,并调用后者的扩展方法GetConnectionString根据指定的连接字符串名称得到完整的连接字符串。接下来我们调用构造函数创建一个DbConfigurationSource对象并注册到ConfigurationBuilder上。创建DbConfigurationSource对象指定的Action<DbContextOptionsBuilder>会完成针对连接字符串的设置。
public static class DbConfigurationExtensions { public static IConfigurationBuilder AddDatabase(this IConfigurationBuilder builder, string connectionStringName, IDictionary<string, string> initialSettings = null) { string connectionString = builder.Build().GetConnectionString(connectionStringName); DbConfigurationSource source = new DbConfigurationSource(optionsBuilder => optionsBuilder.UseSqlServer(connectionString), initialSettings); builder.Add(source); return builder; } }