.net core實現帶名稱的服務(自定義、使用Autofac)
.net core實現了依賴注入,雖然可以滿足大部分的場景了,但是還是有許多不足,其中之一就是實現帶名稱服務的依賴注入。
舉個例子,比如有下面的介面和它的實現類:
public interface IPerson { string Say(); } public class Person1 : IPerson { public virtual string Say() { return nameof(Person1); } } publicclass Person2 : IPerson { public virtual string Say() { return nameof(Person2); } }
然後我們在Startup的ConfigureServices中新增服務:
public void ConfigureServices(IServiceCollection services) { services.AddTransient<IPerson, Person1>(); services.AddTransient<IPerson, Person2>(); ... }
但是當我們注入IPerson服務時,每次得到的都是Person2,因為Person2是比Person1後新增的,想獲取到Person1,我們需要使用其它方法,比如先得到所有IPerson的實現,然後做一個過濾:
var person = serviceProvider.GetService<IPerson>();//每次都是Person2 var persons= serviceProvider.GetServices<IPerson>();//所有IPerson介面的實現:IEnumerable<IPerson>var person1 = persons.FirstOrDefault(p => p is Person1);//過濾得到Person1
這種方式肯定不是我們想要的,那有沒有方法直接獲取Person1?或者說我們能不能給的IPerson的實現類一個名稱,注入的時候使用這個名稱來決定注入那個服務的實現?
然而很可惜,.net core並沒有提供帶名稱的服務注入功能,但是我們可以自己整合實現,有兩種方式:自定義實現,或者採用第三方外掛(如Autofac)
注:以下內容基於.netcore 5.0.14
自定義方式
由於.net core的以來注入本質上是基於服務型別的比較(原始碼見CallSiteFactory的TryCreateOpenGeneric方法和TryCreateExact方法),所以我們可以從這個角度出發,對Type多一個包裝:
先建立一個NamedType:
internal sealed class NamedType : TypeDelegator { object name; public NamedType(object name) { this.name = name; } public NamedType(object name, Type delegatingType) : base(delegatingType) { this.name = name; } public override string Name { get => (name?.GetType() ?? typeof(NamedType)).Name; } public override Guid GUID { get; } = Guid.NewGuid(); public override bool Equals(object obj) => Equals(obj as NamedType); public override int GetHashCode() => name.GetHashCode(); public override bool Equals(Type o) => o is NamedType t && object.Equals(t.name, this.name) && (t.ServiceType == null || ServiceType == null || t.ServiceType == ServiceType); public override string FullName => (name?.GetType() ?? typeof(NamedType)).FullName; public Type ServiceType => typeImpl; }
這個NamedType繼承於TypeDelegator,方便我們自定義封裝。
接著新增一系列的拓展方法,用於新增服務:
public static class ServiceCollectionExtensions { #region NamedScoped public static IServiceCollection AddNamedScoped(this IServiceCollection services, object name, Type serviceType) => services.AddScoped(new NamedType(name, serviceType)); public static IServiceCollection AddNamedScoped(this IServiceCollection services, object name, Type serviceType, Func<IServiceProvider, object> implementationFactory) => services.AddScoped(new NamedType(name, serviceType), implementationFactory); public static IServiceCollection AddNamedScoped(this IServiceCollection services, object name, Type serviceType, Type implementationType) => services.AddScoped(new NamedType(name, serviceType), implementationType); public static IServiceCollection AddNamedScoped<TService>(this IServiceCollection services, object name) where TService : class => services.AddNamedScoped(name, typeof(TService)); public static IServiceCollection AddNamedScoped<TService>(this IServiceCollection services, object name, Func<IServiceProvider, TService> implementationFactory) where TService : class => services.AddNamedScoped(name, typeof(TService), sp => implementationFactory.Invoke(sp)); public static IServiceCollection AddNamedScoped<TService, TImplementation>(this IServiceCollection services, object name) where TService : class where TImplementation : class, TService => services.AddNamedScoped(name, typeof(TService), typeof(TImplementation)); public static IServiceCollection AddNamedScoped<TService, TImplementation>(this IServiceCollection services, object name, Func<IServiceProvider, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService => services.AddNamedScoped(name, typeof(TService), sp => implementationFactory.Invoke(sp)); #endregion #region NamedSingleton public static IServiceCollection AddNamedSingleton(this IServiceCollection services, object name, Type serviceType, Type implementationType) => services.AddSingleton(new NamedType(name, serviceType), implementationType); public static IServiceCollection AddNamedSingleton(this IServiceCollection services, object name, Type serviceType, object implementationInstance) => services.AddSingleton(new NamedType(name, serviceType), implementationInstance); public static IServiceCollection AddNamedSingleton(this IServiceCollection services, object name, Type serviceType, Func<IServiceProvider, object> implementationFactory) => services.AddSingleton(new NamedType(name, serviceType), implementationFactory); public static IServiceCollection AddNamedSingleton(this IServiceCollection services, object name, Type serviceType) => services.AddSingleton(new NamedType(name, serviceType)); public static IServiceCollection AddNamedSingleton<TService>(this IServiceCollection services, object name) where TService : class => services.AddNamedSingleton(name, typeof(TService)); public static IServiceCollection AddNamedSingleton<TService>(this IServiceCollection services, object name, Func<IServiceProvider, TService> implementationFactory) where TService : class => services.AddNamedSingleton(name, typeof(TService), sp => implementationFactory.Invoke(sp)); public static IServiceCollection AddNamedSingleton<TService>(this IServiceCollection services, object name, TService implementationInstance) where TService : class => services.AddNamedSingleton(name, typeof(TService), implementationInstance); public static IServiceCollection AddNamedSingleton<TService, TImplementation>(this IServiceCollection services, object name, Func<IServiceProvider, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService => services.AddNamedSingleton(name, typeof(TService), sp => implementationFactory.Invoke(sp)); public static IServiceCollection AddNamedSingleton<TService, TImplementation>(this IServiceCollection services, object name) where TService : class where TImplementation : class, TService => services.AddNamedSingleton(name, typeof(TService), typeof(TImplementation)); #endregion #region NamedTransient public static IServiceCollection AddNamedTransient(this IServiceCollection services, object name, Type serviceType) => services.AddTransient(new NamedType(name, serviceType)); public static IServiceCollection AddNamedTransient(this IServiceCollection services, object name, Type serviceType, Func<IServiceProvider, object> implementationFactory) => services.AddTransient(new NamedType(name, serviceType), implementationFactory); public static IServiceCollection AddNamedTransient(this IServiceCollection services, object name, Type serviceType, Type implementationType) => services.AddTransient(new NamedType(name, serviceType), implementationType); public static IServiceCollection AddNamedTransient<TService>(this IServiceCollection services, object name) where TService : class => services.AddNamedTransient(name, typeof(TService)); public static IServiceCollection AddNamedTransient<TService>(this IServiceCollection services, object name, Func<IServiceProvider, TService> implementationFactory) where TService : class => services.AddNamedTransient(name, typeof(TService), sp => implementationFactory.Invoke(sp)); public static IServiceCollection AddNamedTransient<TService, TImplementation>(this IServiceCollection services, object name) where TService : class where TImplementation : class, TService => services.AddNamedTransient(name, typeof(TService), typeof(TImplementation)); public static IServiceCollection AddNamedTransient<TService, TImplementation>(this IServiceCollection services, object name, Func<IServiceProvider, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService => services.AddNamedTransient(name, typeof(TService), sp => implementationFactory.Invoke(sp)); #endregion }ServiceCollectionExtensions
為了配合使用,我們還需要一系列的獲取服務實現的拓展方法:
public static class ServiceProviderExtensions { /// <summary> /// 獲取命名服務 /// </summary> /// <param name="provider"></param> /// <param name="name">服務名稱</param> /// <returns></returns> public static object GetNamedService(this IServiceProvider provider, object name) => provider.GetService(new NamedType(name)); /// <summary> /// 獲取命名服務 /// </summary> /// <param name="provider"></param> /// <param name="name">服務名稱</param> /// <returns></returns> public static object GetRequiredNamedService(this IServiceProvider provider, object name) => provider.GetRequiredService(new NamedType(name)); /// <summary> /// 獲取命名服務 /// </summary> /// <param name="provider"></param> /// <param name="name">服務名稱</param> /// <param name="serviceType"></param> /// <returns></returns> public static object GetRequiredNamedService(this IServiceProvider provider, object name, Type serviceType) => provider.GetRequiredService(new NamedType(name, serviceType)); /// <summary> /// 獲取命名服務 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="provider"></param> /// <param name="name">服務名稱</param> /// <returns></returns> public static T GetRequiredNamedService<T>(this IServiceProvider provider, object name) => (T)provider.GetRequiredService(new NamedType(name, typeof(T))); /// <summary> /// 獲取命名服務 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="provider"></param> /// <param name="name">服務名稱</param> /// <returns></returns> public static T GetNamedService<T>(this IServiceProvider provider, object name) => (T)provider.GetService(new NamedType(name, typeof(T))); /// <summary> /// 獲取命名服務 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="provider"></param> /// <param name="name">服務名稱</param> /// <returns></returns> public static IEnumerable<T> GetNamedServices<T>(this IServiceProvider provider, string name) => provider.GetServices(new NamedType(name, typeof(T))).OfType<T>().ToArray(); /// <summary> /// 獲取命名服務 /// </summary> /// <param name="provider"></param> /// <param name="name">服務名稱</param> /// <param name="serviceType"></param> /// <returns></returns> public static IEnumerable<object> GetNamedServices(this IServiceProvider provider, object name, Type serviceType) => provider.GetServices(new NamedType(name, serviceType)).Where(serviceType.IsInstanceOfType).ToArray(); }ServiceProviderExtensions
然後我們可以在Startup的ConfigureServices中新增具名服務:
public void ConfigureServices(IServiceCollection services) { services.AddNamedTransient<IPerson, Person1>("person1"); services.AddNamedTransient<IPerson, Person2>("person2"); ... }
相應的,可以使用名稱獲取對應的服務實現:
var person1 = serviceProvider.GetNamedService<IPerson>("person1");// Person1 var person2 = serviceProvider.GetNamedService<IPerson>("person2");// Person2
以上自定義方式參考了:https://www.jianshu.com/p/ae8991280fb5,在此基礎上有一些拓展,名稱不用侷限於string型別,而是任何的object型別,這樣不僅可以使用一些數值型別作為名稱,也可以使用一些複雜型別,總之,只需要這個型別重寫了Equals方法就可以了,比如record:
public record Named(string FirstName,string LastName); // 一個記錄 public void ConfigureServices(IServiceCollection services) { services.AddNamedTransient<IPerson, Person1>(new Named("zhang","san")); services.AddNamedTransient<IPerson, Person2>(new Named("li", "si")); ... }
可以使用名稱獲取對應的服務實現:
var person1 = serviceProvider.GetNamedService<IPerson>(new Named("zhang", "san"));// Person1 var person2 = serviceProvider.GetNamedService<IPerson>(new Named("li", "si"));// Person2
甚至,這裡還可以使用匿名類:
public void ConfigureServices(IServiceCollection services) { services.AddNamedTransient<IPerson, Person1>(new { a = "zhang", b = "san" }); services.AddNamedTransient<IPerson, Person2>(new { a = "li", b = "si" }); ... }
使用名稱獲取對應的服務實現:
var person1 = serviceProvider.GetNamedService<IPerson>(new { a = "zhang", b = "san" });// Person1 var person2 = serviceProvider.GetNamedService<IPerson>(new { a = "li", b = "si" });// Person2
這樣,我們可以很輕鬆的使用多個項作為名稱,而不需要把它們轉換成單一型別了
這種自定義的方式已經很好用了,但是有它的不足,因為我們服務無法通過建構函式來使用帶名稱的服務注入,只能通過serviceProvider來直接獲取。接下來說說使用autofac來解決這個問題。
採用第三方外掛(Autofac)
Autofac是.net最優秀的IOC外掛之一,從.net framework開始就有了它的存在。
開始,我們需要使.net core整合使用Autofac替換掉原身的IOC,方法如下:
首先,使用nuget安裝:Autofac.Extensions.DependencyInjection
然後修改Program,使用AutofacServiceProviderFactory:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseServiceProviderFactory(new AutofacServiceProviderFactory()) //使用Autofac .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
這樣就替換掉.net core原版的IOC了,是不是很簡單?
注:在以前.netcore2.x及之前的版本,想替換原版的IOC,只需要在ConfigureServices中返回IServiceProvider物件即可:
public IServiceProvider ConfigureServices(IServiceCollection services) { ... var container = new ContainerBuilder(); container.Populate(services); return new AutofacServiceProvider(container); }
從.net core3.x開始,ConfigureServices已經不支援返回IServiceProvider了,需要使用IServiceProviderFactory<TContainerBuilder>介面來指定,比如Autofac對應的實現類是AutofacServiceProviderFactory,所以一般需要在Program中使用它替換預設的IServiceProviderFactory<TContainerBuilder>從而達到替換原版的IOC。而如果我們要使用Autofac的ContainerBuilder,有兩種方式:1、使用AutofacServiceProviderFactory例項化時傳入configurationAction引數,2、在Startup中新增ConfigureContainer方法,引數就是ContainerBuilder,這也是推薦做法。
回到話題,由於.net core的IServiceCollection不能新增帶名稱的服務,所以帶名稱的服務需要使用Autofac的ContainerBuilder來完成,這一點我們可以在Startup的ConfigureContainer中來完成:
public class Startup { ... public void ConfigureContainer(ContainerBuilder containerBuilder) { containerBuilder.RegisterType<Person1>().Keyed<IPerson>("person1"); containerBuilder.RegisterType<Person2>().Keyed<IPerson>("person2"); } ... }
然後在建構函式中使用即可Autofac.Features.AttributeFilters.KeyFilterAttribute指定引數使用那個key就行了,然後執行啟動,直接報錯:
如果我們的服務類是通過ContainerBuilder新增的,那麼需要在新增完成後使用WithAttributeFiltering方法修飾一下,如:
我們有一個Demo類需要注入IPerson:
public class Demo { public Demo([KeyFilter("person1")] IPerson person1, [KeyFilter("person2")] IPerson person2) { Console.WriteLine(person1.Say()); Console.WriteLine(person2.Say()); } }
那麼我們需要在新增Demo的時候:
public void ConfigureContainer(ContainerBuilder containerBuilder) { containerBuilder.RegisterType<Person1>().Keyed<IPerson>("person1"); containerBuilder.RegisterType<Person2>().Keyed<IPerson>("person2"); //WithAttributeFiltering告訴Autofac在例項化時啟用建構函式的引數的特性 containerBuilder.RegisterType<Demo>().WithAttributeFiltering(); }
這樣,Demo中的person1和person2才會唄正確賦值。
如果我們的服務類是通過IServiceCollection新增的,比如Controller,這樣一來我們就無法新增WithAttributeFiltering標識,但是我們可以使用Autofac的Middleware來解決這個問題。
首先,因為預設情形下,Controller不是交給IOC的Service去例項化的,那麼我們首先就得修改這個預設行為,只需要修改ConfigureServices:
public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddControllersAsServices();//AddControllersAsServices表示將Controller的例項化按普通的服務一樣去進行 ... }
接著,定義一個管道處理器,它的作用就類似WithAttributeFiltering的作用:
public class DefaultResolveMiddleware : IResolveMiddleware { public PipelinePhase Phase => PipelinePhase.Sharing; public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next) { var parameter = new ResolvedParameter((p, c) => { var filter = p.GetCustomAttributes<ParameterFilterAttribute>(true).FirstOrDefault(); return filter != null && filter.CanResolveParameter(p, c); }, (p, c) => { var filter = p.GetCustomAttributes<ParameterFilterAttribute>(true).First(); return filter.ResolveParameter(p, c); }); var paramters = context.Parameters.ToList(); paramters.Add(parameter); context.ChangeParameters(paramters);//替換掉原來的 next(context); } }
這個管道處理器其實就等價於啟用WithAttributeFiltering。
然後在ConfigureContainer中,對所有的Controller使用這個管道處理器:
public void ConfigureContainer(ContainerBuilder containerBuilder) { containerBuilder.RegisterType<Person1>().Keyed<IPerson>("person1"); containerBuilder.RegisterType<Person2>().Keyed<IPerson>("person2"); //WithAttributeFiltering告訴Autofac在例項化時啟用建構函式的引數的特性 containerBuilder.RegisterType<Demo>().WithAttributeFiltering(); this.GetType().Assembly.GetTypes() .Where(f => !f.IsAbstract && typeof(ControllerBase).IsAssignableFrom(f)) //得到所有的控制器 .ToList() .ForEach(c => { //對每一個Controller都使用DefaultResolveMiddleware,它的作用等價於使用WithAttributeFiltering宣告 containerBuilder.RegisterServiceMiddleware(new TypedService(c), new DefaultResolveMiddleware()); }); }
這樣,通過IServiceCollection新增的服務也就可以在構造引數中使用帶名稱的服務了。
總結
這裡提供了兩種方法讓.net core支援帶名稱的服務,各有優缺點:
自定義方式: 優點:不需要依賴第三方外掛,實現起來也簡單,可以定製自己的需求,滿足大部分情形 缺點:只能通過IServiceProvider來例項化,不支援在建構函式中注入 Autofac實現方式: 優點:基於Autofac實現的強大依賴注入功能,適合各種需要複雜注入的場景 缺點:依賴於Autofac第三方外掛,而且實現起來稍繁瑣,技術上要求更高一些