功能打散揉碎成模塊之后, 最麻煩的莫過于各個模塊的配置如何加載.
.NET4.8 之前, 可以用自定義的 JsonConfig (讀取 .config 文件太麻煩) 來加載配置,
.NET Core 之后提供了強大的配置系統(tǒng), 如果在使用那個 JsonConfig
就顯的太潦草了.
但是配置分布于各個模塊, 模塊和模塊之間只是通過接口約束, 在這種情況下又如何使用配置呢?
在啟動項目里注冊 ?
一個兩個也就算了, 百八十個的子模塊, 按這樣搞法, 豈不是一團亂麻?
搞過 IoC
自動注冊的, 都知道掃描目錄下的 DLL, 然后 AddSingleton
, AddScoped
, AddTransient
, 這個不成功問題.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class RegistAttribute : Attribute
{
public RegistMode Mode { get; }
public Type ForType { get; }
...
...
var ts = asm.GetExportedTypes();
var tmps = ts.SelectMany(t => t.etCustomAttributes<RegistAttribute>().Select(a => new { t, attr = a }));
foreach (var t in tmps)
{
Regist(sc, t.attr.ForType ?? t.t, t.t, t.attr.Mode);
}
...
...
case RegistMode.Singleton:
sc.AddSingleton(forType, type);
...
...
不便之處
麻煩的是, IServiceCollection.Configure<T>(IConfiguration)
方法需要泛型參數(shù) T
。
基于現(xiàn)有知識教藻,要想用上面注冊 IoC
的方式來注冊配置桶现,那基本是不現(xiàn)實的:
因為 Attribute
目前還沒有正式支持泛型
如果不使用泛型 Attribute, 只能想辦法變通變通了:
通過反射來實現(xiàn)
掃描 DLL 里實現(xiàn)了
ICfg
接口的類型妙蔗, 通過Activator
創(chuàng)建一個實例, 然后調用AutoConfigure
public interface ICfg
{
string Section { get; }
public void AutoConfigure(IServiceCollection sc, IConfiguration configuration);
}
...
...
public abstract class CfgBase<T> : ICfg where T : class
{
public abstract string Section { get; }
public void AutoConfigure(IServiceCollection sc, IConfiguration configuration)
{
sc.Configure<T>(configuration.GetSection(this.Section));
}
}
...
...
public class ServiceCfg : CfgBase<ServiceCfg>
{
public override string Section => "Service";
...
...
var ts = asm.ExportedTypes;
var cfgTypes = ts.Where(t => !t.IsAbstract && !t.IsInterface && t.sAssignableTo(typeof(ICfg)));
foreach (var ct in cfgTypes)
{
var o = (ICfg)Activator.CreateInstance(ct, true);
o.AutoConfigure(sc, configuration);
...
...
這種方法其實還好, 唯一不爽的是夹孔, 必須通過 Activator
來創(chuàng)建一個對象, 然后在進行配置注冊析孽。
通過泛型特性的實現(xiàn)方法
上面說 Attribute
還未正式支持泛型搭伤,意思是說已經(jīng)可以這樣寫了:
public class RegistCfgAttribute<T> : RegistCfgAttribute where T : class
...
...
[RegistCfg<PriceChangeJobCfg>("PriceChange")]
public class PriceChangeJobCfg : BasePriceStockChangeJobCfg
{
...
...
前提是,要啟用 preview
語法支持袜瞬,修改項目文件怜俐, 加入 LangVersion
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
如果項目比較多, 一個一個加比較麻煩邓尤,也可以通過修改:Directory.Build.props
文件 (放到解決方案根目錄下) :
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
這個方法看起來比較清爽拍鲤, 但是是 preview
的, 能不能成為正式的汞扎, 還不好說季稳。
完整示例
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, configuration) =>
{
//以 windows service 運行時, TopShelf 會將 c:\windows\system32 做為 baseDir, 會從這個目錄里加載配置,
//所以, 用 Topshelf + CreateHostBuilder 這種方法的, 需要手動指定 basePath.
//直接 new ConfigurationBuilder() 的貌似沒有這個問題.
var dir = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
configuration.SetBasePath(dir);
//加載各個模塊輸出的配置
var dir2 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cfgs");
var fs = Directory.GetFiles(dir2, "*.json");
foreach (var f in fs)
configuration.AddJsonFile(f, true, true);
})
.ConfigureServices((hostContext, services) =>
{
#region 自動配置, 自動注冊IoC
//通過 ICfg 實現(xiàn)的配置自動注冊
services.AutoConfigure(hostContext.Configuration, Assembly.GetExecutingAssembly());
services.AutoConfigure(hostContext.Configuration);
// 通過泛型 Attribute 實現(xiàn)的配置自動注冊, 需開啟 preview 語法支持澈魄。
services.AutoConfigureByPreview(hostContext.Configuration, Assembly.GetExecutingAssembly());
services.AutoConfigureByPreview(hostContext.Configuration);
//從當前運行的 Assembly 里注冊
services.AutoRegist(Assembly.GetExecutingAssembly());
services.AutoRegist();
#endregion
})
.ConfigureLogging((context, b) => b.AddLog4Net("log4net.config", true));
ICfg
配置類 (通過反射來實現(xiàn)):
public interface ICfg
{
string Section { get; }
public void AutoConfigure(IServiceCollection sc, IConfiguration configuration);
}
public abstract class CfgBase<T> : ICfg where T : class
{
public abstract string Section { get; }
public void AutoConfigure(IServiceCollection sc, IConfiguration configuration)
{
sc.Configure<T>(configuration.GetSection(this.Section));
}
}
public class ProducerCfg : CfgBase<ProducerCfg>
{
public override string Section => "Producer";
public string BrokerServerAddress { get; set; }
}
泛型特性配置類:
public abstract class RegistCfgAttribute : Attribute
{
public abstract void Regist(IServiceCollection sc, IConfiguration configuration);
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class RegistCfgAttribute<T> : RegistCfgAttribute where T : class
{
public string Section { get; }
public RegistCfgAttribute(string section)
{
this.Section = section;
}
public override void Regist(IServiceCollection sc, IConfiguration configuration)
{
sc.Configure<T>(configuration.GetSection(this.Section));
}
}
[RegistCfg<PriceChangeJobCfg>("PriceChange")]
public class PriceChangeJobCfg : BasePriceStockChangeJobCfg
{
public int TaskCount { get; set; } = 5;
}
擴展:
public static class RegistExtensions
{
public static void AutoRegist(this IServiceCollection sc, Assembly asm)
{
try
{
var ts = asm.GetExportedTypes();
var tmps = ts.SelectMany(t => t.GetCustomAttributes<RegistAttribute>().Select(a => new { t, attr = a }));
foreach (var t in tmps)
{
Regist(sc, t.attr.ForType ?? t.t, t.t, t.attr.Mode);
}
}
catch (Exception e)
{
}
}
private static void Regist(IServiceCollection sc, Type forType, Type type, RegistMode mode)
{
switch (mode)
{
case RegistMode.Singleton:
sc.AddSingleton(forType, type);
break;
case RegistMode.Scoped:
sc.AddScoped(forType, type);
break;
case RegistMode.Transient:
sc.AddTransient(forType, type);
break;
}
}
public static void AutoRegist(this IServiceCollection sc, string searchPattern = "CNB.Job.*.dll")
{
var asms = DetectAssemblys(searchPattern);
foreach (var asm in asms)
AutoRegist(sc, asm);
}
public static void AutoConfigure(this IServiceCollection sc, IConfiguration configuration, Assembly asm)
{
try
{
var ts = asm.ExportedTypes;
var cfgTypes = ts.Where(t => !t.IsAbstract && !t.IsInterface && t.IsAssignableTo(typeof(ICfg)));
foreach (var ct in cfgTypes)
{
var o = (ICfg)Activator.CreateInstance(ct, true);
o.AutoConfigure(sc, configuration);
}
}
catch
{
}
}
public static void AutoConfigure(this IServiceCollection sc, IConfiguration configuration, string searchPattern = "CNB.Job.*.dll")
{
var asms = DetectAssemblys(searchPattern);
foreach (var asm in asms)
AutoConfigure(sc, configuration, asm);
}
public static void AutoConfigureByPreview(this IServiceCollection sc, IConfiguration configuration, string searchPattern = "CNB.Job.*.dll")
{
var asms = DetectAssemblys(searchPattern);
foreach (var asm in asms)
AutoConfigureByPreview(sc, configuration, asm);
}
public static void AutoConfigureByPreview(this IServiceCollection sc, IConfiguration configuration, Assembly asm)
{
try
{
var ts = asm.GetExportedTypes();
var tmps = ts.Select(t => t.GetCustomAttribute<RegistCfgAttribute>())
.Where(t => t != null);
foreach (var t in tmps)
{
t.Regist(sc, configuration);
}
}
catch (Exception e)
{
}
}
private static IEnumerable<Assembly> DetectAssemblys(string searchPattern = "CNB.Job.*.dll")
{
var dlls = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, searchPattern);
foreach (var dll in dlls)
{
var asm = Assembly.LoadFrom(dll);
yield return asm;
}
}
}