.net core实现带名称的服务(自定义、使用Autofac)
.net core实现了依赖注入,虽然可以满足大部分的场景了,但是还是有许多不足,其中之一就是实现带名称服务的依赖注入。
举个例子,比如有下面的接口和它的实现类:
public interface IPerson { string Say(); } public class Person1 : IPerson { public virtual string Say() { return nameof(Person1); } } public class 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 }
为了配合使用,我们还需要一系列的获取服务实现的拓展方法:
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(); }
然后我们可以在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"); } ... }
如果是动态获取,可能需要将IServiceProvider转换成AutofacServiceProvider,然后使用它的LifetimeScope的ResolveKeyed方法来获取:
var autofacServiceProvider = serviceProvider as AutofacServiceProvider; //当key是string类型时,Keyed等价于Named var person1 = autofacServiceProvider.LifetimeScope.ResolveKeyed<IPerson>("person1"); var person2 = autofacServiceProvider.LifetimeScope.ResolveNamed<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第三方插件,而且实现起来稍繁琐,技术上要求更高一些