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

}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。