使用.NET Core依赖注入确定在运行时注入哪个实现

14

我的应用程序中有三种用户类型,假设为Type1,Type2和Type3。然后,我想为每种类型创建一个服务实现,例如我有一个获取照片的服务,我会有三个服务:Type1PhotosService,Type2PhotosService和Type3PhotosService,每个服务都实现了IPhotosService

在Web API中,我将注入IPhotosService

IPhotosService _service;

public PhotosController(IPhotosService service){
   _service = service;
} 
Web API使用带有声明的令牌认证。因此,我想实现的是根据用户所拥有的声明:type1、type2或type3,将自动注入正确的服务实现,而不是在startup文件中注入单个服务。我要避免的是拥有一个服务,其中包含一堆switchif语句,以依据用户类型和角色返回正确的数据。
编辑: 有些评论质疑三种实现的意义,因此这里提供更多细节以使其更加有意义。 该服务是一个职位查找服务,并且应用程序具有三种不同的配置文件:candidate、employer和administration。每个配置文件都需要适当的实现。因此,我更喜欢每个配置文件类型有一个实现,然后根据配置文件类型使用正确的实现,而不是在同一个服务中拥有三个方法GetCandidateJobs、GetEmployerJobs和GetAdministrationJobs并基于用户类型进行切换。

3
审查策略模式。我建议这样做是因为根据您的要求,在控制器初始化并注入依赖项时,用户还不知道。因此,直到在控制器已创建并注入其依赖项之后的操作中才会知道您想要什么。 - Nkosi
要么这样,或者注入一个工厂并从中解析服务 photoServiceFactory.GetServiceForUser(User user) ,但说实话,如果您所需的只是基于条件的不同凭据,我不确定为什么您需要三个不同的照片服务实现。 - Tseng
这引起了我的关注的原因是因为我必须在.NET 4.6应用程序中完成所有这些工作。我使用Windsor完成了它,然后发现我不能使用Windsor,所以我没有使用IoC容器再次完成了它。最终,我更喜欢没有Windsor的版本,因为它更简单。再次解决这个问题使我重新确认了这一点。文档在此处记录:(http://scotthannen.org/blog/2018/12/16/liberation-of-pure-di-mef-is-not-ioc.html)。 - Scott Hannen
@Nkosi,你的回答最为恰当,请将其添加为答案,以便我接受。 - badr slaoui
4个回答

8

不使用独立的IoC容器

这里有一种比配置应用程序以使用另一个IoC容器并配置该容器更容易的方法。在使用Windsor进行此操作后,此解决方案似乎要简单得多。

如果您可以使用每个服务实现的单例实例,则此方法最简单。

我们将从一个接口、一些实现以及我们可以注入的工厂开始,该工厂将根据某些输入在运行时返回所选的实现。

public interface ICustomService { }
public class CustomServiceOne : ICustomService { }
public class CustomServiceTwo : ICustomService { }
public class CustomServiceThree : ICustomService { }

public interface ICustomServiceFactory
{
    ICustomService Create(string input);
}

这是一个非常简单的工厂实现示例。(没有使用字符串常量,也没有进行任何优化。)
public class CustomServiceFactory : ICustomServiceFactory
{
    private readonly Dictionary<string, ICustomService> _services 
        = new Dictionary<string, ICustomService>(StringComparer.OrdinalIgnoreCase);

    public CustomServiceFactory(IServiceProvider serviceProvider)
    {
        _services.Add("TypeOne", serviceProvider.GetService<CustomServiceOne>());
        _services.Add("TypeTwo", serviceProvider.GetService<CustomServiceTwo>());
        _services.Add("TypeThree", serviceProvider.GetService<CustomServiceThree>());
    }

    public ICustomService Create(string input)
    {
        return _services.ContainsKey(input) ? _services[input] : _services["TypeOne"];
    }
}

假设您已经使用IServiceCollection注册了CustomServiceOneCustomServiceTwo等服务。它们不会被注册为接口实现,因为我们不是通过这种方式解决它们。这个类只是解决每一个服务并将它们放在一个字典中,以便您可以通过名称检索它们。

在这种情况下,工厂方法采用一个字符串,但您可以检查任何类型或多个参数来确定要返回哪个实现。甚至使用字符串作为字典键也是任意的。并且,仅作为示例,我提供了回退行为来返回一些默认实现。如果您无法确定正确的实现,则抛出异常可能更合理。

根据您的需求,另一种选择是在请求时在工厂内部解决实现。尽可能地,我尝试使大多数类保持无状态,以便我可以解决和重用单个实例。

要在启动时向IServiceCollection注册工厂,我们需要执行以下操作:

services.AddSingleton<ICustomServiceFactory>(provider => 
    new CustomServiceFactory(provider));
IServiceProvider会在工厂被解析时被注入到工厂中,然后工厂将使用它来解析服务。
以下是对应的单元测试。测试方法与Windsor答案中使用的方法相同,这证明我们可以透明地替换一个工厂实现,并在组合根中更改其他内容而不破坏任何东西。
public class Tests
{
    private IServiceProvider _serviceProvider;
    [SetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddSingleton<CustomServiceOne>();
        services.AddSingleton<CustomServiceTwo>();
        services.AddSingleton<CustomServiceThree>();
        services.AddSingleton<ICustomServiceFactory>(provider => 
            new CustomServiceFactory(provider));
        _serviceProvider = services.BuildServiceProvider();
    }

    [TestCase("TypeOne", typeof(CustomServiceOne))]
    [TestCase("TypeTwo", typeof(CustomServiceTwo))]
    [TestCase("TYPEThree", typeof(CustomServiceThree))]
    [TestCase("unknown", typeof(CustomServiceOne))]
    public void FactoryReturnsExpectedService(string input, Type expectedType)
    {
        var factory = _serviceProvider.GetService<ICustomServiceFactory>();
        var service = factory.Create(input);
        Assert.IsInstanceOf(expectedType, service);
    }
}

与温莎示例类似,这篇文章旨在避免在组合根之外引用容器。如果一个类依赖于ICustomServiceFactoryICustomService,你可以在这个实现、温莎实现或者工厂的任何其他实现之间进行切换。

5

使用Windsor

我将回避关于这是否在此情况下有意义的问题,只尝试回答所提出的问题:

.NET Core的IoC容器对于这种情况建立得不是特别好。(他们在文档中承认了这一点。)您可以通过添加像Windsor这样的另一个IoC容器来解决这个问题。

实现最终看起来比我想象的要复杂得多,但一旦完成设置,它就不错,并且您可以访问Windsor的功能。我将提供另一个答案,其中不包括Windsor。我必须完成所有这些工作才能看到我可能更喜欢另一种方法。

在您的项目中,添加Castle.Windsor.MsDependencyInjection NuGet软件包。

用于测试的接口和实现

为了进行测试,我添加了一些接口和实现:

public interface ICustomService { }
public interface IRegisteredWithServiceCollection { }
public class CustomServiceOne : ICustomService { }
public class CustomServiceTwo : ICustomService { }
public class CustomServiceThree : ICustomService { }
public class RegisteredWithServiceCollection : IRegisteredWithServiceCollection { }

目的是创建一个工厂,使用一些运行时输入来选择并返回ICustomService的实现。

这是一个作为工厂的接口。我们可以将其注入到一个类中,在运行时调用它以获取ICustomService的实现:

public interface ICustomServiceFactory
{
    ICustomService Create(string input);
}

配置Windsor容器

下一步是编写一个类来配置IWindsorContainer以解决依赖关系:

public class WindsorConfiguration : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.AddFacility<TypedFactoryFacility>();
        container.Register(
            Component.For<ICustomService, CustomServiceOne>().Named("TypeOne"),
            Component.For<ICustomService, CustomServiceTwo>().Named("TypeTwo"),
            Component.For<ICustomService, CustomServiceThree>().Named("TypeThree"),
            Component.For<ICustomService, CustomServiceOne>().IsDefault(),
            Component.For<ICustomServiceFactory>().AsFactory(new CustomServiceSelector())
        );
    }
}

public class CustomServiceSelector : DefaultTypedFactoryComponentSelector
{
    public CustomServiceSelector()
        : base(fallbackToResolveByTypeIfNameNotFound: true) { }

    protected override string GetComponentName(MethodInfo method, object[] arguments)
    {
       return (string) arguments[0];
    }
}

以下是这里正在进行的内容:

  • TypedFactoryFacility将使我们能够使用Windsor的类型化工厂。它将为我们创建一个工厂接口的实现。
  • 我们注册了三个ICustomService的实现。因为我们注册了多个实现,每个实现都必须有一个名称。当我们解析ICustomService时,我们可以指定一个名称,它将根据该字符串来解析类型。
  • 为了说明问题,我注册了另一个没有名称的ICustomService实现。这将使我们能够在尝试使用无法识别的名称进行解析时解析默认实现。(一些替代方案是抛出异常、返回ICustomService的“null”实例或创建一个像UnknownCustomService这样的类,该类会抛出异常。)
  • Component.For<ICustomServiceFactory>().AsFactory(new CustomServiceSelector())告诉容器创建一个代理类来实现ICustomServiceFactory。(更多信息请参见他们的文档。)
  • CustomServiceSelector是负责获取传递给工厂的Create方法的参数,并返回用于选择组件的组件名称(TypeOne、TypeTwo等)的对象。在本例中,我们期望传递给工厂的参数与我们使用的注册名称相同。但是我们可以用其他逻辑替换它。我们的工厂甚至可以接受其他类型的参数,我们可以检查并确定要返回哪个字符串。

配置您的应用程序以使用Windsor容器

现在,在StartUp中,修改ConfigureServices以返回IServiceProvider而不是void,并创建一个IServiceProvider,将直接注册到IServiceCollection的服务与注册到Windsor容器的服务结合起来:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    var container = new WindsorContainer();
    container.Install(new WindsorConfiguration());
    return WindsorRegistrationHelper.CreateServiceProvider(container, services);
}

container.Install(new WindsorConfiguration()) 允许 WindsorConfiguration 配置我们的容器。我们可以在此方法中直接配置容器,但这是一种保持容器配置组织化的好方法。我们可以创建 numerous IWindsorInstaller 实现或自己的自定义类来配置 Windsor 容器。
WindsorRegistrationHelper.CreateServiceProvider(container, services) 创建使用 containerservicesIServiceProvider

它是否有效?

我不会没有先弄清楚就发布所有这些内容。以下是一些 NUnit 测试。(我通常为 DI 配置编写一些基本测试。)

设置创建类似于应用程序启动时会发生的 IServiceProvider。它创建一个容器并应用 WindsorConfiguration。我还直接向 ServiceCollection 注册服务,以确保两者能够很好地配合。然后将两者合并为一个 IServiceProvider

然后我从 IServiceProvider 中解析出一个 ICustomerServiceFactory,并验证它对于每个输入字符串返回正确的 ICustomService 实现,包括当字符串不是已识别的依赖项名称时的回退。我还验证了直接向 ServiceCollection 注册的服务已被解析。

public class Tests
{
    private IServiceProvider _serviceProvider;
    [SetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IRegisteredWithServiceCollection, RegisteredWithServiceCollection>();
        var container = new WindsorContainer();
        container.Install(new WindsorConfiguration());
        _serviceProvider = WindsorRegistrationHelper.CreateServiceProvider(container, services);
    }

    [TestCase("TypeOne", typeof(CustomServiceOne))]
    [TestCase("TypeTwo", typeof(CustomServiceTwo))]
    [TestCase("TYPEThree", typeof(CustomServiceThree))]
    [TestCase("unknown", typeof(CustomServiceOne))]
    public void FactoryReturnsExpectedService(string input, Type expectedType)
    {
        var factory = _serviceProvider.GetService<ICustomServiceFactory>();
        var service = factory.Create(input);
        Assert.IsInstanceOf(expectedType, service);
    }

    [Test]
    public void ServiceProviderReturnsServiceRegisteredWithServiceCollection()
    {
        var service = _serviceProvider.GetService<IRegisteredWithServiceCollection>();
        Assert.IsInstanceOf<RegisteredWithServiceCollection>(service);
    }
}

这一切值得吗?

现在我已经弄清楚了,如果我真的需要这种功能,我可能会使用它。如果你试图同时使用Windsor和.NET Core,并且第一次看到它的抽象工厂实现,那么情况会更糟。 这里有另一篇文章,介绍了关于Windsor抽象工厂的更多信息,没有关于.NET Core的干扰。


请注意,Castle.Windsor.MsDependencyInjection 不是官方的软件包。相反,请参阅 Castle Windsor 文档中如何使用 ASP.NET Core facility ( https://github.com/castleproject/Windsor/blob/master/docs/aspnetcore-facility.md )。 - Steven
1
@Steven,谢谢,我会看一下并尝试更新这个。 - Scott Hannen

4

我要冒昧地说,试图使用依赖注入来实现这个目的是次优的。通常情况下,可以通过工厂模式来处理,该模式使用可怕的 ifswitch 语句生成服务实现。一个简单的例子如下:

public interface IPhotoService { 
     Photo CreatePhoto(params);
}

public class PhotoServiceFactory {
    private readonly IPhotoService _type1;
    private readonly IPhotoService _type2;
    private readonly IPhotoService _type3;
    public PhotoServiceFactory(IDependency1 d1, IDependency2 d2, ...etc) {
        _type1 = new ConcreteServiceA(d1);
        _type2 = new ConcreteServiceB(d2);
        _type3 = new ConcreteServiceC(etc);
    }
    public IPhotoService Create(User user) {
        switch(user.Claim) {
            case ClaimEnum.Type1:
                return _type1;
            case ClaimEnum.Type2:
                return _type2;
            case ClaimEnum.Type3:
                return _type3;
            default:
                throw new NotImplementedException
        }
    }
}

然后在你的控制器中:

public class PhotosController {
    IPhotoServiceFactory _factory;
    public PhotosController(IPhotoServiceFactory factory){
       _factory = factory;
    } 
    public IHttpActionResult GetPhoto() {
       var photoServiceToUse = _factory.Create(User);
       var photo = photoServiceToUse.CreatePhoto(params);
       return Ok(photo);
    }
}

或者,只需将具体类作为构造函数的参数,并按照与上述类似的逻辑进行操作。


1
我不建议使用名为IType1PhotoServiceIType2PhotoService等的接口来继承IPhotoService。接口的意义在于它不会因实现而变化。(还有,这个例子依赖注入,只是没有IoC容器。)除此之外,我同意这个实现是可以的,特别是因为类将依赖于工厂接口,这意味着如果需要,工厂实现可以更改(这可能永远不会发生)。 - Scott Hannen
@ScottHannen 我同意你的观点,即命名接口并不是最好的选择,但为了更好地支持Liskov替换原则,我会在一定程度上牺牲一些“接口隔离原则”。 - C Bauer
如果该工厂将依赖于实现接口的三种不同类型,那么它们可能同样是具体类型。这将消除额外接口的需求。 - Scott Hannen
我宁愿不使用服务定位器在工厂内实例化依赖项,除非您的意思是完全不使用接口进行注册? - C Bauer
仔细思考后,我会在工厂中新建ConcreteTypeA/B/C,并将接口依赖项作为构造函数参数传递给抽象工厂。我查阅了Ploeh的博客,但我非常不同意他提出的即使在“基础设施组件”的情况下也要使用服务定位器。我更希望消费者了解他们的依赖关系,而不是在任何级别上依赖于组合根。 - C Bauer

1

这里有一个解决方案,我已经创建了一个在 ASP.NET Core 控制台应用程序内。

using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;

namespace CreationalPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            // Add dependency into service collection
            var services = new ServiceCollection()
                .AddTransient<FordFigoFactory>()
                .AddTransient<AudiQ7Factory>();

            /* Create CarServiceFactory as singleton because it can be used across the application more frequently*/
            services.AddSingleton<ICarServiceFactory>(provider => new CarServiceFactory(provider));            

            // create a service provider from the service collection
            var serviceProvider = services.BuildServiceProvider();


            /* instantiate car*/
            var factory = serviceProvider.GetService<ICarServiceFactory>();
            var audiCar = factory.Create("audi").CreateACar("Blue");            

            Console.Read();
        }
    }


    public interface ICarServiceFactory
    {
        ICreateCars Create(string input);
    }

    public class CarServiceFactory : ICarServiceFactory
    {
        private readonly Dictionary<string, ICreateCars> _services
            = new Dictionary<string, ICreateCars>(StringComparer.OrdinalIgnoreCase);

        public CarServiceFactory(IServiceProvider serviceProvider)
        {
            _services.Add("ford", serviceProvider.GetService<FordFigoFactory>());
            _services.Add("audi", serviceProvider.GetService<AudiQ7Factory>());               
        }

        public ICreateCars Create(string input)
        {
            Console.WriteLine(input + " car is created.");
            return _services.ContainsKey(input) ? _services[input] : _services["ford"];
        }
    }


    public interface ICreateCars
    {
        Car CreateACar(string color);        

    }

    public class FordFigoFactory : ICreateCars
    {
        public Car CreateACar(string color)
        {
            Console.WriteLine("FordFigo car is created with color:" + color);

            return new Fordigo { Color = color};
        }
    }

    public class AudiQ7Factory : ICreateCars
    {
        public Car CreateACar(string color)
        {
            Console.WriteLine("AudiQ7 car is created with color:" + color);

            return new AudiQ7 { Color = color };
        }
    }

    public abstract class Car
    {
        public string Model { get; set; }
        public string Color { get; set; }
        public string Company { get; set; }        
    }

    public class Fordigo : Car
    {
        public Fordigo()
        {
            Model = "Figo";
            Company = "Ford";
        }
    }

    public class AudiQ7 : Car
    {
        public AudiQ7()
        {
            Model = "Audi";
            Company = "Q7";
        }
    }
}

说明: 为更好地理解,尝试从下向上阅读程序。我们有三个部分:

  1. 汽车(Car,Fordigo,AudiQ7)
  2. 汽车工厂(ICreateCars,FordFigoFactory,AudiQ7Factory)
  3. 汽车服务(ICarServiceFactory,CarServiceFactory)

在这里,依赖注入被注册为工厂类 FordFigoFactory 和 AudiQ7Factory 的瞬态,以及 CarServiceFactory 的单例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接