【问题标题】:Determining which implementation to inject at runtime using .NET Core dependency injection使用 .NET Core 依赖注入确定在运行时注入哪个实现
【发布时间】:2019-05-26 09:56:40
【问题描述】:

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

在 web api 中,我会注入 IPhotosService

IPhotosService _service;

public PhotosController(IPhotosService service){
   _service = service;
} 

Web API 使用带有声明的令牌身份验证。所以我想要实现的是,对于每个用户,根据他的声明:type1 or type2 or type3,服务的正确实现将自动注入,而不是在startup 文件中注入单个服务。 我要避免的是拥有一项服务,其中包含一堆 switchif 语句,根据用户类型和他所拥有的角色返回正确的数据。

编辑: 一些 cmets 想知道这三种实现的意义何在,所以这里有更多的细节来让它更有意义。 该服务是一项工作查找服务,该应用程序具有三个不同的配置文件:candidate, employer and administration。这些配置文件中的每一个都需要适当的实现。因此,与其在同一个服务中使用三个方法GetCandidateJobs, GetEmployerJobs and GetAdministrationJobs 并打开用户类型,我更喜欢每个配置文件类型有一个实现,然后根据配置文件类型,使用正确的实现。

【问题讨论】:

  • 查看策略模式。我建议,因为根据您的需要,在初始化控制器并注入依赖项时,用户还不知道。因此,在创建控制器并注入其依赖项之后,直到在操作中才知道您想要什么。
  • 要么注入工厂,要么注入工厂并从中解析服务photoServiceFactory.GetServiceForUser(User user),但老实说,如果您需要的只是基于条件的不同凭据,我不确定为什么需要 3 种不同的照片服务实现
  • 引起我注意的原因是我必须在 .NET 4.6 应用程序中完成所有这些工作。我用 Windsor 做了,然后发现我不允许使用 Windsor,所以我在没有 IoC 容器的情况下又做了一次。我最终更喜欢没有温莎的版本,因为它更简单。再次通过它来回答这个问题再次证实了这一点。它记录在here
  • @Nkosi 您有最合适的答案,请将其添加为答案,以便我接受它

标签: c# asp.net asp.net-mvc asp.net-core dependency-injection


【解决方案1】:

使用温莎

我将回避关于这在这种情况下是否有意义的问题,并尝试按要求回答问题:

.NET Core 的 IoC 容器并不是特别适合这种场景。 (他们在他们的文档中承认了这一点。)您可以通过添加另一个 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);
}

配置温莎容器

接下来是一个类,它将配置一个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&lt;ICustomServiceFactory&gt;().AsFactory(new CustomServiceSelector()) 告诉容器创建一个代理类来实现ICustomServiceFactory。 (更多信息请参见their documentation。)
  • 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 配置我们的容器。我们可以用这种方法直接配置容器,但这是保持容器配置井井有条的好方法。我们可以创建大量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 结合使用,并且第一次看到它是抽象工厂实现,那看起来会更糟。 Here's another article 提供了有关 Windsor 抽象工厂的更多信息,而没有关于 .NET Core 的所有噪音。

【讨论】:

  • 请注意,Castle.Windsor.MsDependencyInjection 不是官方包。相反,请参阅 Castle Windsor 文档中如何使用 ASP.NET Core facility
  • @Steven,谢谢,我看看能不能更新一下。
【解决方案2】:

我要在这里冒昧地说一下使用依赖注入来达到这个目的的尝试是次优的。通常这将由使用可怕的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);
    }
}

或者,只使用具体类作为构造函数中的参数,并遵循与上述类似的逻辑。

【讨论】:

  • 我不建议使用继承 IPhotoService 的名为 IType1PhotoServiceIType2PhotoService 等的接口。接口的要点是它不会因实现而异。 (而且,令人毛骨悚然的细节 - 这个例子 依赖注入,只是没有 IoC 容器。)除此之外,我同意这个实现是好的,特别是因为类将依赖于工厂接口,这意味着如果需要,可以更改工厂实现(这可能永远不会发生。)
  • @ScottHannen 我同意你的观点,命名接口并不是最好的东西,但我牺牲了一点 Interface Segregation Principal 以更好地支持 @987654329 @.
  • 如果工厂将依赖于实现接口的三种不同类型,那么它们也可能是具体类型。这将消除对额外接口的需求。
  • 我不希望使用服务定位器在工厂内部实例化依赖项,除非您的意思是在没有接口的情况下注册它们?
  • 仔细考虑,我只是在工厂中新建了ConcreteTypeA/B/C,并将接口依赖项作为构造函数参数传递给抽象工厂。我查阅并阅读了 Ploeh 博客,但我非常不同意使用服务定位器,即使在他建议的“基础设施组件”的情况下也是如此。我更希望消费者了解他们的依赖关系,然后在任何级别依赖组合根。
【解决方案3】:

不使用单独的 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);
    }
}

就像在 Windsor 示例中一样,这是为了避免对组合根之外的容器的任何引用。如果一个类依赖于ICustomServiceFactoryICustomService,您可以在此实现、Windsor 实现或工厂的任何其他实现之间切换。

【讨论】:

    【解决方案4】:

    这是一种解决方案,我在 asp.net 核心控制台应用程序中创建。

    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";
            }
        }
    }
    

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

    1. 汽车(汽车、Fordigo、AudiQ7)
    2. CarFactory(ICreateCars、FordFigoFactory、AudiQ7Factory)
    3. 汽车服务(ICarServiceFactory、CarServiceFactory)

    在此依赖注入注册为工厂类 FordFigoFactory 和 AudiQ7Factory 的瞬态。以及 CarServiceFactory 的 Singleton。

    【讨论】:

      猜你喜欢
      • 2020-12-20
      • 1970-01-01
      • 2016-05-22
      • 1970-01-01
      • 1970-01-01
      • 2023-03-14
      • 2017-06-09
      • 2021-10-23
      • 1970-01-01
      相关资源
      最近更新 更多