.NET 5/6 配置自動注冊 AutoConfigure

功能打散揉碎成模塊之后, 最麻煩的莫過于各個模塊的配置如何加載.

.NET4.8 之前, 可以用自定義的 JsonConfig (讀取 .config 文件太麻煩) 來加載配置,

.NET Core 之后提供了強大的配置系統(tǒng), 如果在使用那個 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) 方法需要泛型參數(shù) T

基于現(xiàn)有知識教藻,要想用上面注冊 IoC 的方式來注冊配置桶现,那基本是不現(xiàn)實的:

因為 Attribute 目前還沒有正式支持泛型

如果不使用泛型 Attribute, 只能想辦法變通變通了:

通過反射來實現(xiàn)

掃描 DLL 里實現(xiàn)了 ICfg 接口的類型妙蔗, 通過 Activator 創(chuàng)建一個實例, 然后調用 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 來創(chuàng)建一個對象, 然后在進行配置注冊析孽。

通過泛型特性的實現(xiàn)方法

上面說 Attribute 還未正式支持泛型搭伤,意思是說已經(jīng)可以這樣寫了:

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 實現(xiàn)的配置自動注冊
            services.AutoConfigure(hostContext.Configuration, Assembly.GetExecutingAssembly());
            services.AutoConfigure(hostContext.Configuration);

            // 通過泛型 Attribute 實現(xiàn)的配置自動注冊, 需開啟 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 配置類 (通過反射來實現(xiàn)):

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

}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末景鼠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子痹扇,更是在濱河造成了極大的恐慌铛漓,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鲫构,死亡現(xiàn)場離奇詭異浓恶,居然都是意外死亡,警方通過查閱死者的電腦和手機结笨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門问顷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人禀梳,你說我怎么就攤上這事杜窄。” “怎么了算途?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵塞耕,是天一觀的道長。 經(jīng)常有香客問我嘴瓤,道長扫外,這世上最難降的妖魔是什么莉钙? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮筛谚,結果婚禮上磁玉,老公的妹妹穿的比我還像新娘。我一直安慰自己驾讲,他們只是感情好蚊伞,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吮铭,像睡著了一般时迫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谓晌,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天掠拳,我揣著相機與錄音,去河邊找鬼纸肉。 笑死溺欧,一個胖子當著我的面吹牛,可吹牛的內容都是我干的柏肪。 我是一名探鬼主播胧奔,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼预吆!你這毒婦竟也來了?” 一聲冷哼從身側響起胳泉,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤拐叉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扇商,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凤瘦,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年案铺,在試婚紗的時候發(fā)現(xiàn)自己被綠了蔬芥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡控汉,死狀恐怖笔诵,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情姑子,我是刑警寧澤乎婿,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站街佑,受9級特大地震影響谢翎,放射性物質發(fā)生泄漏捍靠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一森逮、第九天 我趴在偏房一處隱蔽的房頂上張望榨婆。 院中可真熱鬧,春花似錦褒侧、人聲如沸良风。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拖吼。三九已至,卻和暖如春这吻,著一層夾襖步出監(jiān)牢的瞬間吊档,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工唾糯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留怠硼,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓移怯,卻偏偏與公主長得像香璃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子舟误,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容