.NET 5/6 配置自动注册 AutoConfigure
功能打散揉碎成模块之后, 最麻烦的莫过于各个模块的配置如何加载.
.NET4.8 之前, 可以用自定义的 JsonConfig (读取 .config 文件太麻烦) 来加载配置,
.NET Core 之后提供了强大的配置系统, 如果在使用那个 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)
方法需要泛型参数 T
。
基于现有知识,要想用上面注册 IoC
的方式来注册配置,那基本是不现实的:
因为 Attribute
目前还没有正式支持泛型
如果不使用泛型 Attribute, 只能想办法变通变通了:
通过反射来实现
扫描 DLL 里实现了
ICfg
接口的类型, 通过Activator
创建一个实例, 然后调用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
来创建一个对象, 然后在进行配置注册。
通过泛型特性的实现方法
上面说 Attribute
还未正式支持泛型,意思是说已经可以这样写了:
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 实现的配置自动注册
services.AutoConfigure(hostContext.Configuration, Assembly.GetExecutingAssembly());
services.AutoConfigure(hostContext.Configuration);
// 通过泛型 Attribute 实现的配置自动注册, 需开启 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
配置类 (通过反射来实现):
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;
}
}
}