美文网首页
.NET 5/6 配置自动注册 AutoConfigure

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

作者: gruan | 来源:发表于2021-11-10 16:46 被阅读0次

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

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

    相关文章

      网友评论

          本文标题:.NET 5/6 配置自动注册 AutoConfigure

          本文链接:https://www.haomeiwen.com/subject/ljhizltx.html