【问题标题】:Injecting multiple implementations with Dependency injection使用依赖注入注入多个实现
【发布时间】:2023-03-14 08:21:01
【问题描述】:

我目前正在开发一个 ASP.NET Core 项目,并希望使用内置的依赖注入 (DI) 功能。

嗯,我从一个界面开始:

ICar
{
    string Drive();
}

并想多次实现ICar接口

public class BMW : ICar
{
    public string Drive(){...};
}

public class Jaguar : ICar
{
    public string Drive(){...};
}

并在Startup 类中添加以下内容

public void ConfigureServices(IServiceCollection services)
{
     // Add framework services.
     services.AddMvc();
     services.AddTransient<ICar, BMW>(); 
     // or 
     services.AddTransient<ICar, Jaguar>();
 }

现在我必须在两个实现之间做出决定,我决定的类将设置在每个需要 ICar 实现的构造函数中。 但我的想法是,如果请求的 Controller 是 BMWController,则使用 BMW 实现,如果请求 JaguarController,则使用 Jaguar

否则 DI 对我来说没有意义。我该如何正确处理这个问题?

为了更好地理解我的问题,看看这张照片:https://media-www-asp.azureedge.net/media/44907/dependency-injection-golf.png?raw=true 依赖解析器是如何工作的,我可以在 ASP.NET Core 的什么地方设置它?

在 Unity 中可以制作这样的东西 container.RegisterType&lt;IPerson, Male&gt;("Male"); container.RegisterType&lt;IPerson, Female&gt;("Female");

并像这样调用正确的类型

[Dependency("Male")]IPerson malePerson

【问题讨论】:

  • 我认为这个想法是错误的。如果您有需要具体汽车实现的 BMWController,那么注入 ICar 没有任何意义——即 DI 的魔力——你说“给我 ICar 的实现”而不关心它是什么。我会准备一个单独的接口:interface IBmwCar: ICar { ... } 并将其注入BMWController。另一个想法是创建工厂。 f.e class CarFactory : ICarFactory { ICar GimmeCar&lt;TController&gt;() } 您已将控制器类型映射到具体实现。
  • 如果我使用 IBmwCar 之类的东西,这不是接口溢出吗:我想要实现的所有品牌的 ICar?我只有 Drive 作为方法,不会有任何其他方法。好吧,我会创建 IBMW,IJaguar... 与 ICar 相同的签名,并为我实现的每个品牌使用这样的声明: services.AddTransient();显然我可以使用工厂模式,但没有任何解决方案可以使用 DI 来完成我的工作吗?
  • 关于 DI 的一件事是您可以依赖 IEnumerable 并获取所有已注册的实现,如果没有注册任何实现,您将获得一个空列表而不是失败解决它。所以如果 ICar 有一些标识符属性,你可以在列表中找到你需要的那个。

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


【解决方案1】:

您正在寻找的功能不容易实现,至少当您在控制器中使用它时,因为控制器被特殊对待(默认情况下,控制器未注册ServiceCollection,因此未解析/由容器实例化,而在请求期间由 ASP.NET Core 实例化,另请参见 my related answer 上的说明和示例。

使用内置的 IoC 容器,您只能通过工厂方法来实现,这里以 BmwCarFactory 类为例:

services.AddScoped<ICar, BmwCar>();
services.AddScoped<BmwCar>();
services.AddScoped<BmwCarFactory>(p => new BmwCarFactory(p.GetRequiredService<BmwCar>())));

默认 IoC 容器有意保持简单,以提供依赖注入的基础知识以帮助您入门,并让其他 IoC 容器能够轻松地插入其中并替换默认实现。

对于更高级的场景,鼓励用户使用他们选择的支持更高级功能(程序集扫描、装饰器、条件/参数化依赖项等)的 IoC。

AutoFac(我在我的项目中使用)支持这种高级场景。在 AutoFac 文档中,有 4 种情况(连同 @pwas 在 cmets 中建议的第 3 种情况):

##1。重新设计你的课程 重构代码和类层次结构需要一些额外开销,但大大简化了注入服务的消耗

##2。更改注册 如果您不愿意或无法更改代码,文档将其描述为 here

// Attach resolved parameters to override Autofac's
// lookup just on the ISender parameters.
builder.RegisterType<ShippingProcessor>()
       .WithParameter(
         new ResolvedParameter(
           (pi, ctx) => pi.ParameterType == typeof(ISender),
           (pi, ctx) => ctx.Resolve<PostalServiceSender>()));
builder.RegisterType<CustomerNotifier>();
       .WithParameter(
         new ResolvedParameter(
           (pi, ctx) => pi.ParameterType == typeof(ISender),
           (pi, ctx) => ctx.Resolve<EmailNotifier>()));
var container = builder.Build();

##3。使用密钥服务 (here) 它与之前的方法 2 非常相似。但基于密钥而不是具体类型来解析服务

##4。使用元数据 这与 3 非常相似。但是您通过属性定义键。

Unity 等其他容器具有特殊属性,例如 DependencyAttribute,您可以使用它来注释依赖关系,例如

public class BmwController : Controller
{
    public BmwController([Dependency("Bmw")ICar car)
    {
    }
}

但是这个和 Autofac 的第四个选项会使 IoC 容器泄漏到您的服务中,您应该考虑其他方法。

或者,您可以创建基于某些约定解析服务的类和工厂。例如ICarFactory:

public ICarFactory
{
    ICar Create(string carType);
}

public CarFactory : ICarFactory
{
    public IServiceProvider provider;

    public CarFactory(IServiceProvider provider)
    {
        this.provider = provider;
    }

    public ICar Create(string carType)
    {
        if(type==null)
            throw new ArgumentNullException(nameof(carType));

        var fullQualifedName = $"MyProject.Business.Models.Cars.{carType}Car";
        Type carType = Type.GetType(fullQualifedName);
        if(carType==null)
            throw new InvalidOperationException($"'{carType}' is not a valid car type.");

        ICar car = provider.GetService(carType);
        if(car==null)
            throw new InvalidOperationException($"Can't resolve '{carType.Fullname}'. Make sure it's registered with the IoC container.");

        return car;
    }
}

然后像这样使用它

public class BmwController : Controller
{
    public ICarFactory carFactory;

    public BmwController(ICarFactory carFactory)
    {
        this.carFactory = carFactory;

        // Get the car
        ICar bmw = carFactory.Create("Bmw");
    }
}

##IServiceProvider 的替代品

// alternatively inject IEnumerable<ICar>
public CarFactory : ICarFactory
{
    public IEnumerable<ICar> cars;

    public CarFactory(IEnumerable<ICar> cars)
    {
        this.cars = cars;
    }

    public ICar Create(string carType)
    {
        if(type==null)
            throw new ArgumentNullException(nameof(carType));

        var carName = "${carType}Car";
        var car = cars.Where(c => c.GetType().Name == carName).SingleOrDefault();

        if(car==null)
            throw new InvalidOperationException($"Can't resolve '{carName}.'. Make sure it's registered with the IoC container.");

        return car;
    }
}

【讨论】:

  • 还使用IEnumerable&lt;ICar&gt; 为@JoeAudette 的评论添加了替代方案,以避免对容器的依赖
  • 这正是我想要的,谢谢!我之前在我的项目中使用过 Unity,我想知道在 ASP.NET Core 的 nativ DI 中是否有可能这样做。我得到了答案谢谢!
  • 我对第 1 点还有一个额外的问题,以便更好地理解。一个接口应该只有一个实现吗?
  • 视情况而定。它可以有多种实现,但您在注册时只能选择一种。示例可能是 IUserRepository 具有 SqliteUserRepositorySqlServerUserRepositrory 并且您只注册其中一个。如果您注册了多个注册,您将无法根据上下文轻松注入它们,或者您使用IEnumerable&lt;T&gt; 解决所有这些注册。因此,如果您想通过 IBmwCar 不使用其他选项,那么是的,它应该只有一个实现
  • 感谢您提供如此详细的答案。当我们有多个实现时,我们通常依靠 DI 来解决基于注册的服务。只是想知道这种情况是否还有其他副作用,除了我们对容器的依赖增加。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-10-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多