.NET 5/6 配置自动注册 AutoConfigure

399 阅读4分钟

功能打散揉碎成模块之后, 最麻烦的莫过于各个模块的配置如何加载.

.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;
        }
    }

}