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

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容