#4. 自定義配置提供程式 在 .NET Core 配置系統中封裝一個配置提供程式關鍵在於提供相應的 IconfigurationSource 實現和 IConfigurationProvider 介面實現,這兩個介面在上一章 ASP.NET Core - 配置系統之配置提供程式 中也有提到了。 I ...
4. 自定義配置提供程式
在 .NET Core 配置系統中封裝一個配置提供程式關鍵在於提供相應的 IconfigurationSource 實現和 IConfigurationProvider 介面實現,這兩個介面在上一章 ASP.NET Core - 配置系統之配置提供程式 中也有提到了。
IConfigurationSource
IConfigurationSource負責創建IConfigurationProvider實現的實例。它的定義很簡單,就一個Build方法,返回IConfigurationProvider實例:
public interface IConfigurationSource
{
IConfigurationProvider Build(IConfigurationBuilder builder);
}
IConfigurationProvider
IConfigurationProvider 負責實現配置的設置、讀取、重載等功能,並以鍵值對形式提供配置。
public interface IConfigurationProvider
{
// 獲取指定父路徑下的直接子節點Key,然後 Concat(earlierKeys) 一同返回
IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
// 當該配置提供程式支持更改追蹤(change tracking)時,會返回 change token
// 否則,返回 null
IChangeToken GetReloadToken();
// 載入配置
void Load();
// 設置 key:value
void Set(string key, string value);
// 嘗試獲取指定 key 的 value
bool TryGet(string key, out string value);
}
像工作中常用的配置中心客戶端,例如 nacos、consul,都是實現了對應的配置提供程式,從而將配置中心中的配置無縫地接入到 .NET Core 的配置系統中進行使用,和本地配置文件的使用沒有分別。
如果我們需要封裝自己的配置提供程式,推薦直接繼承抽象類 ConfigurationProvider,該類實現了 IConfigurationProvider 介面,繼承自該類只要實現Load方法即可,Load方法用於從配置來源載入解析配置信息,將最終的鍵值對配置信息存儲到Data中。這個過程中可參考一下其他已有的配置提供程式的源碼,模仿著去寫自己的東西。
在我們日常的系統平臺中,總少不了數據字典這樣一個功能,用於維護平臺中一些業務配置,因為是隨業務動態擴展和變動的,很多時候不會寫在配置文件,而是維護在資料庫中。以下以這樣一個場景實現一個配置提供程式。
因為是以資料庫作為載體來存儲配置信息,所以第一步就是定義實體類
public class DataDictioaryDO
{
public int Id { get; set
public int? ParentId { get; set
public string Key { get; set
public string Value { get; set; }
}
數據字典支持多級級聯,通過ParentId關聯上一級,ParentId為空的即為根節點,如存在下級節點則Value值可以為空,就算填寫了也無效,最終呈現出來的就是一個樹結構。
然後就是定義相應的資料庫訪問上下問 DataDictionaryDbContext
public class DataDictionaryDbContext : DbContext
{
public DbSet<DataDictioaryDO> DataDictioaries { get; set; }
public DataDictionaryDbContext(DbContextOptions<DataDictionaryDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<DataDictioaryDO>().HasKey(e => e.Id);
modelBuilder.Entity<DataDictioaryDO>().Property(e => e.Value).IsRequired(false);
}
}
通過 DbContextOptions 交由外部去配置具體的資料庫類型和連接字元串。
之後創建 IConfigurationSource 實現類,主要就是構造函數中需要傳入資料庫配置委托,並且在 Build 實例化EFDataDictionaryConfigurationProvider 對象。
public class EFDataDictionaryConfigurationSource : IConfigurationSource
{
private readonly Action<DbContextOptionsBuilder> _action;
public EFDataDictionaryConfigurationSource(Action<DbContextOptionsBuilder> action)
{
_action= action;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new EFDataDictionaryConfigurationProvider(_action);
}
}
之後通過繼承 ConfigurationProvider 實現 EFDataDictionaryConfigurationProvider,主要邏輯就是從資料庫獲取對應的數據表,如果表中沒有數據則插入預設數據,再通過相應的解析器解析數據表數據生成一個 Dictionary<string, string> 對象。
public class EFDataDictionaryConfigurationProvider : ConfigurationProvider
{
Action<DbContextOptionsBuilder> OptionsAction { get; }
public EFDataDictionaryConfigurationProvider(Action<DbContextOptionsBuilder> action)
{
OptionsAction = action;
}
public override void Load()
{
var builder = new DbContextOptionsBuilder<DataDictionaryDbContext>();
OptionsAction(builde);
using var dbContext = new DataDictionaryDbContext(builder.Options);
if(dbContext == null)
{
throw new Exception("Null DB Context !");
}
dbContext.Database.EnsureCreated();
if (!dbContext.DataDictioaries.Any())
{
CreateAndSaveDefaultValues(dbContext);
}
Data = EFDataDictionaryParser.Parse(dbContext.DataDictioaries);
}
private void CreateAndSaveDefaultValues(DataDictionaryDbContext context)
{
var datas = new List<DataDictioaryDO>
{
new DataDictioaryDO
{
Id = 1,
Key = "Settings",
},
new DataDictioaryDO
{
Id = 2,
ParentId = 1,
Key = "Provider",
Value = nameof(EFDataDictionaryConfigurationProvider)
},
new DataDictioaryDO
{
Id = 3,
ParentId = 1,
Key = "Version",
Value = "v1.0.0"
}
};
context.DataDictioaries.AddRange(datas);
context.SaveChanges();
}
}
其中,解析器 EFDataDictionaryParser 的代碼如下,主要就是通過遞歸的方式,通過樹形數據的 key 構建構建完整的 key,並將其存入 Dictionary<string,string>對象中。
internal class EFDataDictionaryParser
{
private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _context = new();
private string _currentPath;
private EFDataDictionaryParser() { }
public static IDictionary<string, string> Parse(IEnumerable<DataDictioaryDO> datas) =>
new EFDataDictionaryParser().ParseDataDictionaryConfiguration(datas);
private IDictionary<string, string> ParseDataDictionaryConfiguration(IEnumerable<DataDictioaryDO> datas)
{
_data.Clear();
if(datas?.Any() != true)
{
return _data;
}
var roots = datas.Where(d => !d.ParentId.HasValue);
foreach (var root in roots)
{
EnterContext(root.Key);
VisitElement(datas, root);
ExitContext();
}
return _data;
}
private void VisitElement(IEnumerable<DataDictioaryDO> datas, DataDictioaryDO parent)
{
var children = datas.Where(d => d.ParentId == parent.Id);
if (children.Any())
{
foreach (var section in children)
{
EnterContext(section.Key);
VisitElement(datas, section);
ExitContext();
}
}
else
{
var key = _currentPath;
if (_data.ContainsKey(key))
throw new FormatException($"A duplicate key '{key}' was found.");
_data[key] = parent.Value;
}
}
private void EnterContext(string context)
{
_context.Push(context);
_currentPath = ConfigurationPath.Combine(_context.Reverse());
}
private void ExitContext()
{
_context.Pop();
_currentPath = ConfigurationPath.Combine(_context.Reverse());
}
}
之後為這個配置提供程式提供一個擴展方法,方便之後的使用,如下:
public static class EFDataDictionaryConfigurationExtensions
{
public static IConfigurationBuilder AddEFDataDictionaryConfiguration(this IConfigurationBuilder builder,
Action<DbContextOptionsBuilder> optionAction)
{
builder.Add(new EFDataDictionaryConfigurationSource(optionAction));
return builder;
}
}
之後在入口文件中將我們的配置擴展程式添加到配置系統中,並指定使用記憶體資料庫進行測試
using ConfigurationSampleConsole.ConfigProvider;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using var host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// 清除原有的配置提供程式
config.Sources.Clear();
config.AddEFDataDictionaryConfiguration(builder =>
{
builder.UseInMemoryDatabase("DataDictionary");
});
})
.Build();
var configuration = host.Services.GetService<IConfiguration>();
Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:Version: {configuration.GetValue<string>("Settings:version")}");
host.Run();
最後的控制台輸出結果如下:
以上就是 .NET Core 框架下配置系統的一部分知識點,更加詳盡的介紹大家可以再看看官方文檔。配置系統很多時候是結合選項系統一起使用的,下一篇將介紹一下 .NET Core 框架下的選項系統。
參考文章:
ASP.NET Core 中的配置 | Microsoft Learn
配置 - .NET | Microsoft Learn
理解ASP.NET Core - 配置(Configuration)
ASP.NET Core 系列:
目錄:ASP.NET Core 系列總結
上一篇:ASP.NET Core - 配置系統之配置提供程式