為什么做這么一個工具
因為我們的系統往往時面向接口編程的,所以在開發Asp .net core項目的時候,一定會有大量大接口及其對應的實現要在ConfigureService
注冊到ServiceCollection
中,傳統的做法是加了一個服務,我們就要注冊一次(service.AddService()
),又比如,當一個接口有多個實現,在構造函數中獲取服務也不是很友好,而據我所知, .Net Core目前是沒有什么自帶的庫或者方法解決這些問題,當然,如果引入第三方容器如AutoFac這些問題時能迎刃而解的,但是如何在不引入第三方容器來解決這個問題呢?
所以我就設計了這樣的一個輕量級工具.
首先,放上該項目的Github地址(記得Star哦!!)
https://github.com/liuzhenyulive/CodeDi
CodeDi是一個基於 .Net Standard的工具庫,它能幫助我們自動地在Asp .net core或者 .net core項目中完成服務的注冊.
Overview
CodeDi 是 Code Dependency Injection的意思,在上次我在看了由依樂祝寫的<.NET Core中的一個接口多種實現的依賴注入與動態選擇看這篇就夠了>后,回想起我之前遇到的那些問題,感覺撥雲見日,所以,我就開始着手寫這個工具了.
如何使用CodeDi
安裝Nuget包
CodeDi的Nuget包已經發布到了 nuget.org,您可以通過以下指令在您的項目中安裝CodeDi
PM> Install-Package CodeDi
ConfigureServices中的配置
方法 1
您可以在Startup
的ConfigureService
方法中添加AddCodeDi完成對CodeDi的調用.服務的注冊CodeDi會自動為您完成.
public void ConfigureServices(IServiceCollection services)
{
services.AddCoreDi();
services.AddMvc();
}
方法 2
您也可以在AddCodeDi方法中傳入一個Action<CodeDiOptions>
參數,在這個action中,您可以對CodeDiOptions的屬性進行配置.
public void ConfigureServices(IServiceCollection services)
{
services.AddCoreDi(options =>
{
options.DefaultServiceLifetime = ServiceLifetime.Scoped;
});
services.AddMvc();
}
方法 3
當然您也可以直接給AddCodeDi()
方法直接傳入一個CodeDiOptions
實例.
public void ConfigureServices(IServiceCollection services)
{
services.AddCoreDi(new CodeDiOptions()
{
DefaultServiceLifetime = ServiceLifetime.Scoped
});
services.AddMvc();
}
你也可以在appsetting.json
文件中配置CodeDiOptions
的信息,並通過Configuration.Bind("CodeDiOptions", options)
把配置信息綁定到一個CodeDiOptions
實例.
appsetting.json file
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"CodeDiOptions": {
"DefaultServiceLifetime": 1,
"AssemblyNames": [
"*CodeDi"
],
"AssemblyPaths": [
"C:\\MyBox\\Github\\CodeDI\\CodeDI\\bin\\Debug\\netstandard2.0"
],
"IgnoreAssemblies": [
"*Test"
],
"IncludeSystemAssemblies": false,
"IgnoreInterface": [
"*Say"
],
"InterfaceMappings": {
"*Say": "*English"
},
"ServiceLifeTimeMappings": {
"*Say": 0
}
}
}
ConfigureService方法
public void ConfigureServices(IServiceCollection services)
{
var options=new CodeDiOptions();
Configuration.Bind("CodeDiOptions", options);
services.AddCoreDi(options);
services.AddMvc();
}
CodeDiOptions詳解
屬性名稱 | 屬性描述 | 數據類型 | 默認值 |
---|---|---|---|
AssemblyPaths | 在指定目錄下加載Dll程序集 | string[] | Bin目錄 |
AssemblyNames | 選擇要加載的程序集名稱 (支持通配符) | string[] | * |
IgnoreAssemblies | 忽略的程序集名稱 (支持通配符) | string[] | null |
IncludeSystemAssemblies | 是否包含系統程序集(當為false時,會忽略含有System,Microsoft,CppCodeProvider,WebMatrix,SMDiagnostics,Newtonsoft關鍵詞和在App_Web,App_global目錄下的程序集) | bool | false |
IgnoreInterface | 忽略的接口 (支持通配符) | string[] | null |
InterfaceMappings | 接口對應的服務 (支持通配符) ,當一個接口有多個實現時,如果不進行配置,則多個實現都會注冊到SerciceCollection中 | Dictionary<string, string> | null |
DefaultServiceLifetime | 默認的服務生命周期 | ServuceLifetime( Singleton,Scoped,Transient) | ServiceLifetime.Scope |
ServiceLifeTimeMappings | 指定某個接口的服務生命周期,不指定為默認的生命周期 | Dictionary<string, ServiceLifetime> | null |
InterfaceMappings
如果 ISay
接口有SayInChinese
和SayInEnglish
兩個實現,我們只想把SayInEnglish注冊到ServiceCollection
中
public interface ISay
{
string Hello();
}
public class SayInChinese:ISay
{
public string Hello()
{
return "您好";
}
}
public class SayInEnglish:ISay
{
public string Hello()
{
return "Hello";
}
}
那么我們可以這樣配置InterfaceMappings
.
options.InterfaceMappings=new Dictionary<string, string>(){{ "ISay", "SayInChinese" } }
也就是{接口名稱
(支持通配符),實現名稱
(支持通配符)}
ServiceLifeTimeMappings
如果我們希望ISay接口的服務的生命周期為Singleton
,我們可以這樣配置ServiceLifeTimeMappings
.
options.ServiceLifeTimeMappings = new Dictionary<string, ServiceLifetime>(){{"*Say",ServiceLifetime.Singleton}};
也就是也就是{接口名稱
(支持通配符),Servicelifetime
}
關於ServiceLifetime: https://github.com/aspnet/DependencyInjection/blob/master/src/DI.Abstractions/ServiceLifetime.cs
獲取服務實例
當然, 您可以和之前一樣,直接在構造函數中進行依賴的注入,但是當某個接口有多個實現而且都注冊到了ServiceCollection中,獲取就沒有那么方便了,您可以用ICodeDiServiceProvider
來幫助您獲取服務實例.
例如,當 ISay
接口有 SayInChinese
和 SayInEnglish
兩個實現, 我們我們如何獲取我們想要的服務實例呢?
public interface ISay
{
string Hello();
}
public class SayInChinese:ISay
{
public string Hello()
{
return "您好";
}
}
public class SayInEnglish:ISay
{
public string Hello()
{
return "Hello";
}
}
public class HomeController : Controller
{
private readonly ISay _say;
public HomeController(ICodeDiServiceProvider serviceProvider)
{
_say = serviceProvider.GetService<ISay>("*Chinese");
}
public string Index()
{
return _say.Hello();
}
}
ICodeDiServiceProvider.GetService<T>(string name=null)
參數中的Name支持通配符.
CodeDi如何實現的?
既然是一個輕量級工具
,那么實現起來自然不會太復雜,我來說說比較核心的代碼.
private Dictionary<Type, List<Type>> GetInterfaceMapping(IList<Assembly> assemblies)
{
var mappings = new Dictionary<Type, List<Type>>();
var allInterfaces = assemblies.SelectMany(u => u.GetTypes()).Where(u => u.IsInterface);
foreach (var @interface in allInterfaces)
{
mappings.Add(@interface, assemblies.SelectMany(a =>
a.GetTypes().
Where(t =>
t.GetInterfaces().Contains(@interface)
)
)
.ToList());
}
return mappings;
}
GetInterfaceMapping通過反射機制,首先獲取程序集中的所有接口allInterfaces
,然后遍歷allInterfaces
找到該接口對應的實現,最終,該方法返回接口和實現的匹配關系,為Dictionary<Type, List
private void AddToService(Dictionary<Type, List<Type>> interfaceMappings)
{
foreach (var mapping in interfaceMappings)
{
if (mapping.Key.FullName == null || (_options.IgnoreInterface != null &&
_options.IgnoreInterface.Any(i => mapping.Key.FullName.Matches(i))))
continue;
if (mapping.Key.FullName != null && _options.InterfaceMappings != null &&
_options.InterfaceMappings.Any(u => mapping.Key.FullName.Matches(u.Key)))
{
foreach (var item in mapping.Value.Where(value => value.FullName != null).
Where(value => value.FullName.Matches(_options.InterfaceMappings.FirstOrDefault(u => mapping.Key.FullName.Matches(u.Key)).Value)))
{
AddToService(mapping.Key, item);
}
continue;
}
foreach (var item in mapping.Value)
{
AddToService(mapping.Key, item);
}
}
}
該方法要判斷CodeDiOptions中是否忽略了該接口,同時,是否指定實現映射關系.
什么叫實現映射關系呢?參見InterfaceMappings
如果指定了,那么就按指定的來實現,如果沒指定,就會把每個實現都注冊到ServiceCollection中.
private readonly IServiceCollection _service;
private readonly CodeDiOptions _options;
private readonly ServiceDescriptor[] _addedService;
public CodeDiService(IServiceCollection service, CodeDiOptions options)
{
_service = service ?? throw new ArgumentNullException(nameof(service));
_options = options ?? new CodeDiOptions();
_addedService = new ServiceDescriptor[service.Count];
service.CopyTo(_addedService, 0);
//在構造函數中,我們通過這種方式把Service中已經添加的服務讀取出來
//后面進行服務注冊時,會進行判斷,避免重復添加
}
private void AddToService(Type serviceType, Type implementationType)
{
ServiceLifetime serviceLifetime;
try
{
serviceLifetime = _options.DefaultServiceLifetime;
if (_options.ServiceLifeTimeMappings != null && serviceType.FullName != null)
{
var lifeTimeMapping =
_options.ServiceLifeTimeMappings.FirstOrDefault(u => serviceType.FullName.Matches(u.Key));
serviceLifetime = lifeTimeMapping.Key != null ? lifeTimeMapping.Value : _options.DefaultServiceLifetime;
}
}
catch
{
throw new Exception("Service Life Time Only Can be set in range of 0-2");
}
if (_addedService.Where(u => u.ServiceType == serviceType).Any(u => u.ImplementationType == implementationType))
return;
_service.Add(new ServiceDescriptor(serviceType, implementationType, serviceLifetime));
}
AddToService中,要判斷有沒有對接口的生命周期進行配置,參見ServiceLifeTimeMappings,如果沒有配置,就按DefaultServiceLifetime進行配置,DefaultServiceLifetime如果沒有修改的情況下時ServiceLifetime.Scoped,即每個Request創建一個實例.
private readonly IServiceProvider _serviceProvider;
public CodeDiServiceProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public T GetService<T>(string name) where T : class
{
return _serviceProvider.GetService<IEnumerable<T>>().FirstOrDefault(u => u.GetType().Name.Matches( name));
}
這CodeDiServiceProvider的實現代碼,這里參考了依樂祝寫的<.NET Core中的一個接口多種實現的依賴注入與動態選擇看這篇就夠了>給出的一種解決方案,即當某個接口注冊了多個實現,其實可以通過IEnumerable
Enjoy it
只要進行一次簡單的CodeDi配置,以后系統中添加了新的接口以及對應的服務實現后,就不用再去一個個地Add到IServiceCollection中了.
如果有問題,歡迎Issue,歡迎PR.
最后,賞個Star唄! 前往Star