NET 5/6 配置自动注册 AutoConfigure

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

.NET4.8 之前, 可以用自定义的 JsonConfig (读取 .config 文件太麻烦) 来加载配置,

.NET Core 之后提供了强大的配置系统, 如果在使用那个 JsonConfig 就显的太潦草了.

但是配置分布于各个模块, 模块和模块之间只是通过接口约束, 在这种情况下又如何使用配置呢?

在启动项目里注册 ?

一个两个也就算了, 百八十个的子模块, 按这样搞法, 岂不是一团乱麻?


搞过 IoC 自动注册的, 都知道扫描目录下的 DLL, 然后 AddSingleton, AddScoped, AddTransient, 这个不成功问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c#复制代码[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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
c#复制代码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 还未正式支持泛型,意思是说已经可以这样写了:

1
2
3
4
5
6
7
8
c#复制代码public class RegistCfgAttribute<T> : RegistCfgAttribute where T : class
...
...
[RegistCfg<PriceChangeJobCfg>("PriceChange")]
public class PriceChangeJobCfg : BasePriceStockChangeJobCfg
{
...
...

前提是,要启用 preview 语法支持,修改项目文件, 加入 LangVersion

1
2
3
4
xml复制代码<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>

如果项目比较多, 一个一个加比较麻烦,也可以通过修改:Directory.Build.props 文件 (放到解决方案根目录下) :

1
2
3
4
5
xml复制代码<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>

这个方法看起来比较清爽, 但是是 preview 的, 能不能成为正式的, 还不好说。


完整示例

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
c#复制代码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 配置类 (通过反射来实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c#复制代码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; }
}

泛型特性配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c#复制代码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;
}

扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
c#复制代码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;
}
}

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%