最近在忙於 Fireasy 的重構,3.x 拋棄了 .Net Framework 時代的一些思想和模式,緊密擁抱 .Net Core,但它的思想仍然是開放性和靈活性。今天我主要來說說依賴註入與服務發現。 .Net Core 有自己的一套依賴註入,它的容器暴露給 IServiceCollection, ...
最近在忙於 Fireasy 的重構,3.x 拋棄了 .Net Framework 時代的一些思想和模式,緊密擁抱 .Net Core,但它的思想仍然是開放性和靈活性。今天我主要來說說依賴註入與服務發現。
.Net Core 有自己的一套依賴註入,它的容器暴露給 IServiceCollection,通過在裡面放入一些單例(Singleton)、瞬時(Transient)、作用域(Scoped)的一些服務描述(服務與實現的關係映射),這一部分我就不再細說了。
當然,一般常用的方式是,通過 AddSingleton、AddTransient 和 AddScoped 方法往容器裡面加,但如果是依賴比較多的情況下(比如業務服務類),那你可能會經常忘了寫這一部分代碼了,而且也很難於維護。如常見的方式:
void ConfigureServices(IServiceCollection services) { services.AddTransient<IDeptmentService, DeptmentService>(); services.AddTransient<IRoleService, RoleService>(); services.AddTransient<IUserService, UserService>(); services.AddTransient<IDataRoleService, DataRoleService>(); //....... services.AddTransient<IProcessService, ProcessService>(); services.AddTransient<IWorkService, WorkService>(); }
有沒有更簡便更容易維護的方式呢?答案是當然有!
在 Fireasy,我們定義了三個服務介面,分別是 ISingletonService、ITransientService 和 IScopedService,這三個類只是一個標識,沒有具體的方法和屬性。使用需要註入的類實現此介面,如下:
public class DeptmentService : IDeptmentService, ITransientService { // ...... } public class DataRoleHelper : IDataRoleHelper, ISingletonService { // ...... }
好了,你只需在 ConfigureServices 里添加上這麼一行代碼,就能實現依賴註入:
void ConfigureServices(IServiceCollection services) { services.AddFireasy(); }
現在開始步入正題了,來看看 AddFireasy 是如何工作的。
IServiceDiscoverer 是用於服務發現的介面,它的預設實現是 DefaultServiceDiscoverer。如下:
public static SetupBuilder AddFireasy(this IServiceCollection services, Action<SetupOptions>? configure = null) { var options = new SetupOptions(); configure?.Invoke(options); var builder = new SetupBuilder(services, options); var discoverer = options.DiscoverOptions.DiscovererFactory == null ? new DefaultServiceDiscoverer(services, options.DiscoverOptions) : options.DiscoverOptions.DiscovererFactory(services, options.DiscoverOptions); if (discoverer != null) { services.AddSingleton<IServiceDiscoverer>(discoverer); } return builder; }
入口方法是 DiscoverServices,它會遍列程式目錄下的所有程式集文件(*.dll),這裡有程式集過濾器,你可以自己定義過濾規則。如下:
/// <summary> /// 發現工作目錄中所有程式集中的依賴類型。 /// </summary> /// <param name="services"></param> private void DiscoverServices(IServiceCollection services) { foreach (var assembly in GetAssemblies()) { if (_options?.AssemblyFilters?.Any(s => s.IsFilter(assembly)) == true) { continue; } if (_options?.AssemblyFilterPredicates?.Any(s => s(assembly)) == true) { continue; } _assemblies.Add(assembly); ConfigureServices(services, assembly); DiscoverServices(services, assembly); } }
方法 DiscoverServices 用於對單個程式集進行服務發現併進行註冊,這裡同樣也有類型過濾器,如下:
/// <summary> /// 發現程式集中的所有依賴類型。 /// </summary> /// <param name="services"></param> /// <param name="assembly"></param> private void DiscoverServices(IServiceCollection services, Assembly assembly) { foreach (var type in assembly.GetExportedTypes()) { if (_options?.TypeFilters?.Any(s => s.IsFilter(assembly, type)) == true) { continue; } if (_options?.TypeFilterPredicates?.Any(s => s(assembly, type)) == true) { continue; } ServiceLifetime? lifetime; var interfaceTypes = type.GetDirectImplementInterfaces().ToArray(); //如果使用標註 if (type.IsDefined(typeof(ServiceRegisterAttribute))) { lifetime = type.GetCustomAttribute<ServiceRegisterAttribute>()!.Lifetime; } else { lifetime = GetLifetimeFromType(type); } if (lifetime == null) { continue; } if (interfaceTypes.Length > 0) { interfaceTypes.ForEach(s => AddService(services, s, type, (ServiceLifetime)lifetime)); } else { AddService(services, type, type, (ServiceLifetime)lifetime); } } } private ServiceLifetime? GetLifetimeFromType(Type type) { if (typeof(ISingletonService).IsAssignableFrom(type)) { return ServiceLifetime.Singleton; } else if (typeof(ITransientService).IsAssignableFrom(type)) { return ServiceLifetime.Transient; } else if (typeof(IScopedService).IsAssignableFrom(type)) { return ServiceLifetime.Scoped; } return null; } private ServiceDescriptor AddService(IServiceCollection services, Type serviceType, Type implType, ServiceLifetime lifetime) { var descriptor = ServiceDescriptor.Describe(serviceType, implType, lifetime); _descriptors.Add(descriptor); services.Add(descriptor); return descriptor; }
從上面的代碼中可看出,通過在程式集內部查找實現了 ISingletonService、ITransientService 或 IScopedService 的類,並將它們添加到 services 中,這樣就完成了開篇提到的工作。
這裡還出現了一個 ServiceRegisterAttribute,它在不實現以上三個介面的情況下,通過標註 Lifetime 生命周期來進行註冊,一樣達到了目的。
接下來做幾個簡單的單元測試。
單例測試:
/// <summary> /// 測試單例服務 /// </summary> [TestMethod] public void TestSingletonService() { var services = new ServiceCollection(); var builder = services.AddFireasy(); var serviceProvider = services.BuildServiceProvider(); var service1 = serviceProvider.GetService<ITestSingletonService>(); var service2 = serviceProvider.GetService<ITestSingletonService>(); Assert.IsNotNull(service1); Assert.IsNotNull(service2); //兩對象的id應相等 Assert.AreEqual(service1.Id, service2.Id); } public interface ITestSingletonService { Guid Id { get; } void Test(); } public class TestSingletonServiceImpl : ITestSingletonService, ISingletonService { public TestSingletonServiceImpl() { Id = Guid.NewGuid(); } public Guid Id { get; } public void Test() => Console.WriteLine("Hello TestSingletonService!"); }
瞬時測試:
/// <summary> /// 測試瞬時服務 /// </summary> [TestMethod] public void TestTransientService() { var services = new ServiceCollection(); var builder = services.AddFireasy(); var serviceProvider = services.BuildServiceProvider(); var service1 = serviceProvider.GetService<ITestTransientService>(); var service2 = serviceProvider.GetService<ITestTransientService>(); Assert.IsNotNull(service1); Assert.IsNotNull(service2); //兩對象的id應不相等 Assert.AreNotEqual(service1.Id, service2.Id); } public interface ITestTransientService { Guid Id { get; } void Test(); } public class TestTransientServiceImpl : ITestTransientService, ITransientService { public TestTransientServiceImpl() { Id = Guid.NewGuid(); } public Guid Id { get; } public void Test() => Console.WriteLine("Hello TestTransientService!"); }
作用域測試:
/// <summary> /// 測試作用域服務 /// </summary> [TestMethod] public void TestScopedService() { var services = new ServiceCollection(); var builder = services.AddFireasy(); var serviceProvider = services.BuildServiceProvider(); Guid id1, id2; //作用域1 using (var scope1 = serviceProvider.CreateScope()) { var service1 = scope1.ServiceProvider.GetService<ITestScopedService>(); var service2 = scope1.ServiceProvider.GetService<ITestScopedService>(); Assert.IsNotNull(service1); Assert.IsNotNull(service2); //兩對象的id應相等 Assert.AreEqual(service1.Id, service2.Id); id1 = service1.Id; } //作用域2 using (var scope2 = serviceProvider.CreateScope()) { var service1 = scope2.ServiceProvider.GetService<ITestScopedService>(); var service2 = scope2.ServiceProvider.GetService<ITestScopedService>(); Assert.IsNotNull(service1); Assert.IsNotNull(service2); //兩對象的id應相等 Assert.AreEqual(service1.Id, service2.Id); id2 = service1.Id; } //兩次scoped的id應不相等 Assert.AreNotEqual(id1, id2); } public interface ITestScopedService { Guid Id { get; } void Test(); } public class TestScopedServiceImpl : ITestScopedService, IScopedService { public TestScopedServiceImpl() { Id = Guid.NewGuid(); } public Guid Id { get; } public void Test() => Console.WriteLine("Hello TestScopedService!"); }
可見,不需要顯式 Add 也能將大量的服務類註入到容器中,不僅節省了大量的時間和代碼,更是提高了程式的可維護性。
最後,奉上 Fireasy 3 的開源地址:https://gitee.com/faib920/fireasy3 ,歡迎大家前來捧場。
本文相關代碼請參考 https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common/DependencyInjection 下的相關文件。
作者:黃旭東出處:http://fireasy.cnblogs.com
版權聲明:本文的版權歸作者與博客園共有。轉載時須註明本文的詳細鏈接,否則作者將保留追究其法律責任。