01

编程体验

我们将这个自定义ConfigurationSource命名为DbConfigurationSource。在正式对它的实现展开介绍之前,我们先来看看它在项目中的应用。我们创建一个控制台程序来演示对这个DbConfigurationSource应用。我们将配置保存在SQL Server数据库中的某个数据表中,并采用Entity Framework Core来读取配置,所以我们需要添加针对“ Microsoft.EntityFrameworkCore”和“Microsoft.EntityFrameworkCore.SqlServer”这两个NuGet包的依赖。

我们将链接字符串作为配置定义在一个名为“connectionString.json”的JSON文件中,所以我们添加了针对NuGet包“Microsoft.Extensions.Configuration.Json”的依赖。链接字符串采用如下的形式定义在这个JSON文件中的定义。

{
  "connectionStrings": {
    "defaultDb":  "Server = ... ; Database=...; Uid = ...; Pwd = ..."
  }
}

我们编写了如下的程序来演示针对自定义DbConfigurationSource的应用。我们首先创建了一个ConfigurationBuilder对象,并注册了一个指向“connectionString.json”文件的JsonConfigurationSource。

var initialSettings = new Dictionary<stringstring>
{
    ["Gender"] = "Male",
    ["Age"] = "18",
    ["ContactInfo:EmailAddress"] = "foobar@outlook.com",
    ["ContactInfo:PhoneNo"] = "123456789"
};

var config = new ConfigurationBuilder()
    .AddJsonFile("connectionString.json")
    .AddDatabase("DefaultDb", initialSettings)
    .Build();

var 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");

针对DbConfigurationSource的注册体现在扩展方法AddDatabase上,这个方法接收两个参数,它们分别代表链接字符串的名称和初始的配置数据。前者正是在配置文件中设置的连接字符串名称“defaultDb”,后者是一个字典对象,它提供的原始配置正好可以构成一个Profile对象。在利用ConfigurationBuilder创建出相应的IConfiguration对象之后,我们采用标准的Options编程模式读取配置将将其绑定为一个Profile对象。

02

DbConfigurationSource 

如上面的代码片断所示,针对DbConfigurationSource的应用仅仅体现在我们为IConfigurationBuilder定义的扩展方法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 { getset; }

    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 { getset; }
}

如下所示的是DbConfigurationSource的定义,它的构造函数接受两个参数,第一个参数类型为Action<DbContextOptionsBuilder>的委托对象,我们用它来对创建DbContext采用的DbContextOptions进行设置,另一个可选的参数用来指定一些需要自动初始化的配置项。DbConfigurationSource在重写的Build方法中利用这两个对象创建一个DbConfigurationProvider对象。

public class DbConfigurationSource : IConfigurationSource
{
    private Action<DbContextOptionsBuilder> _setup;
    private IDictionary<stringstring> _initialSettings;

    public DbConfigurationSource(
        Action<DbContextOptionsBuilder> setup, 
        IDictionary<stringstring> initialSettings = null
)
    
{
        _setup = setup;
        _initialSettings = initialSettings;
    }
    public IConfigurationProvider Build(
        IConfigurationBuilder builder
)
    
=> new DbConfigurationProvider(_setup, _initialSettings);
}

03

DbConfigurationProvider

DbConfigurationProvider派生于抽象类ConfigurationProvider。在重写的Load方法中,它会根据提供的Action<DbContextOptionsBuilder>创建ApplicationSettingsContext对象,并利用后者从数据库中读取配置数据并转换成字典对象并赋值给代表配置字典的Data属性。如果数据表中没有数据,该方法还会利用这个DbContext对象将提供的初始化配置添加到数据库中。

public class DbConfigurationProvider
    : ConfigurationProvider
{
    private IDictionary<stringstring> _initialSettings;
    private Action<DbContextOptionsBuilder> _setup;

    public DbConfigurationProvider(
        Action<DbContextOptionsBuilder> setup, 
        IDictionary<stringstring> initialSettings
)
    
{
        _setup = setup;
        _initialSettings = initialSettings?? new Dictionary<stringstring>() ;
    }

    public override void Load()
    
{
        var builder = 
            new DbContextOptionsBuilder<ApplicationSettingsContext>();
        _setup(builder);
        using (ApplicationSettingsContext dbContext = 
            new ApplicationSettingsContext(builder.Options))
        {
            dbContext.Database.EnsureCreated();
            Data = dbContext.Settings.Any()
                ? dbContext.Settings.ToDictionary(it => it.Key, it => it.Value, StringComparer.OrdinalIgnoreCase)
                : this.Initialize(dbContext);
        }
    }

    private IDictionary<stringstringInitialize(
        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);
    }
}

04

扩展方法

实例演示中用来注册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<stringstring> initialSettings = null
)
    
{
        var connectionString = builder.Build()
            .GetConnectionString(connectionStringName);
        var source = new DbConfigurationSource(
            optionsBuilder => optionsBuilder.UseSqlServer(connectionString), 
            initialSettings);
        builder.Add(source);
        return builder;
    }
}

https://mp.weixin.qq.com/s/E2FBwOfIS9vl6--q9im6Hw