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

时间:2018-12-28 15:33:07

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

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

在网络api中,我将注入IPhotosService

IPhotosService _service;

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

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

编辑: 一些评论想知道这三种实现的意义是什么,所以这里有更多细节可以使它更加有意义。 该服务是一个求职者服务,该应用程序具有三个不同的配置文件:candidate, employer and administration。这些配置文件中的每一个都需要适当的实现。因此,与其在同一个服务中使用三种方法GetCandidateJobs, GetEmployerJobs and GetAdministrationJobs并打开用户类型,不如我对每种配置文件类型使用一个实现,然后根据配置文件类型使用正确的实现。

4 个答案:

答案 0 :(得分:2)

使用温莎

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

.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将使我们能够使用温莎的类型化工厂。它将为我们创建工厂接口的实现。
  • 我们正在注册ICustomService的三种实现。因为我们要注册多个实现,所以每个实现都必须有一个名称。解析ICustomService时,我们可以指定一个名称,它将根据该字符串解析类型。
  • 为说明起见,我注册了ICustomService的另一种实现而没有名称。如果我们尝试使用无法识别的名称进行解析,这将使我们能够解析默认实现。 (某些替代方法只是引发异常,或者返回ICustomService的“空”实例,或者创建诸如UnknownCustomService之类的引发异常的类。)
  • Component.For<ICustomServiceFactory>().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)创建使用IServiceProvidercontainer的{​​{1}}。

行得通吗?

如果不先查找,我不会发布所有这些内容。这是一些NUnit测试。 (我通常会为DI配置编写一些基本测试。)

安装程序将创建一个services,类似于应用程序启动时会发生的情况。它创建一个容器并应用IServiceProvider。我还将直接在WindsorConfiguration上注册服务,以确保两者可以很好地配合使用。然后,我将两者合并为ServiceCollection

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

ServiceCollection

这一切值得吗?

现在我已经弄清楚了,如果我真的需要这种功能,我可能会使用它。如果您想同时使用Windsor和.NET Core ,并且第一次看到它是抽象工厂实现,那看起来会更糟。 Here's another article,并提供了有关Windsor抽象工厂的更多信息,而没有关于.NET Core的所有干扰。

答案 1 :(得分:2)

我将在这里走出去,说为此目的利用依赖注入的尝试是次优的。通常,这将由Factory模式处理,该模式使用可怕的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);
    }
}

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

答案 2 :(得分:0)

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

这假定您已经向CustomServiceOne注册了CustomServiceTwoIServiceCollection等。它们不会被注册为接口实现,因为这不是我们解决它们的方式。此类将简单地解决每个问题并将它们放入字典中,以便您可以按名称检索它们。

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

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

要在启动时向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实现或工厂的任何其他实现之间切换。

答案 3 :(得分:0)

这是我在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. 汽车(Car,Fordigo,AudiQ7)
  2. CarFactory(ICreateCars,FordFigoFactory,AudiQ7Factory)
  3. CarService(ICarServiceFactory,CarServiceFactory)

在此依赖注入中,工厂类FordFigoFactory和AudiQ7Factory被注册为瞬态。还有Singleton用于CarServiceFactory。