问题 模型类(实体)中的依赖注入


我正在使用Entity Framework Code-First构建ASP.NET Core MVC应用程序。 我选择实现一个简单的存储库模式,为我创建的所有模型类提供基本的CRUD操作。 我选择遵循提供的所有建议 http://docs.asp.net 而DI就是其中之一。

在.NET 5中,依赖注入非常适用于我们不直接实例化的任何类(例如:控制器,数据存储库,......)。

我们只需通过构造函数注入它们,并在应用程序的Startup类中注册映射:

// Some repository class
public class MyRepository : IMyRepository
{
    private readonly IMyDependency _myDependency;
    public MyRepository(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }
}

// In startup.cs :
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyRepository, MyRepository>();

我遇到的问题是,在我的一些模型类中,我想注入一些我声明的依赖项。

但我认为我不能使用构造函数注入模式,因为模型类通常是明确地实现的,因此,我需要为自己提供依赖项,我不能。

所以我的问题是:是否有另一种方式比构造函数注入注入依赖项,以及如何?我是在考虑属性模式或类似的东西。


12953
2018-03-24 18:17


起源

由于您正在显式创建模型,因此DI框架无法为您运行并填充依赖项,即使存在某种模式。话虽如此:我们在谈论什么样的依赖?你的模型实例需要什么? - poke
好的经验法则:永远不要“新”任何东西。总是有DI处理它。建议观看: 深入研究依赖注入和编写解耦质量码和可测试软件 - mason
@ kall2sollies我认为你问题中的repostory例子并没有真正帮助传达你关于创建一个问题的问题 模型 对象并在其中注入依赖项。您可能想要更改您的问题,以便您的实际意图变得更加清晰。 - poke
@ kall2sollies:如果您的模型类需要强制依赖,那么您的应用程序设计会出现根本性的错误。试着详细说明 为什么 您需要在模型中使用此依赖项 - Tseng
通常,您可以使用方法注入来注入某项服务(即 IVatCalculator 进入你的模型课 order.CalculateVat(vatCalculator)。该 IVatCalculator 可以使用“公共货币计算(货币,国家/地区)”方法从数据库中获取税率,并根据增值税率进行计算,然后返回计算值并将其分配到模型中。我可以发布一个例子,作为你如何使用一个或多个服务以DDD为中心的方式来做这个问题,如果这是你想通过将逻辑移动到模型来实现的,称为富域模型 - Tseng


答案:


正如我在评论中已经解释的那样,在使用时创建对象 new,进程中涉及的依赖注入框架没有任何内容。因此,DI框架不可能神奇地将东西注入该对象,它根本不知道它。

因为让DI框架没有任何意义 创建 你的模型实例(模型不是 依赖),如果你想让模型拥有它们,你必须明确地传入你的依赖项。你如何做到这一点取决于你的模型用于什么,以及这些依赖是什么。

简单明了的情况是让你的模型期望构造函数的依赖性。这样,如果您不提供它们,则是编译时错误,并且模型可以立即访问它们。因此,无论如何,创建模型都需要具有模型类型所需的依赖关系。但在该级别,这可能是一个服务或控制器,可以访问DI并可以请求依赖本身。

当然,根据依赖项的数量,这可能会变得有点复杂,因为您需要将它们全部传递给构造函数。因此,一种替代方案是拥有一些负责创建模型对象的“模型工厂”。另一种选择也是使用 服务定位器模式通过了 IServiceCollection 到模型,然后可以请求它需要的任何依赖项。请注意,这通常是一种不好的做法,而不再是真正的控制反转。

这两个想法都存在修改对象创建方式的问题。有些模型,特别是那些由Entity Framework处理的模型,需要一个空构造函数才能使EF能够创建对象。那么在那时你可能会最终得到 一些案例 模型的依赖关系  解决了(你没有简单的说法)。

一种通常更好的方法,也就是更明确的方法,就是传递你需要的依赖关系,例如:如果您在模型上有一些计算某些东西但需要一些配置的方法,那么让该方法需要该配置。这也使得方法更容易测试。

另一种解决方案是将逻辑移出模型。比如说 ASP.NET身份模型 真的很蠢。他们什么都不做。所有的逻辑都是在 UserStore 这是一种服务,因此可以具有服务依赖性。


6
2018-03-24 18:49



事实上这是可能的,正如我在答案中详细说明的那样。你可以手动利用 CallContextServiceLocator.Locator.ServiceProvider 使用手动实例化的类来解决依赖关系。 - David Pine
@DavidPine虽然这不是依赖注入。这是非常丑陋的服务定位器模式,具有对静态(全局)对象的硬依赖性。另外,我不知道你指的是什么 “这个” 在 “这实际上是可能的”。你看起来好像我的回答是两行长。 - poke
两者都是通过添加来控制的 IInterface 至 Implementation 在里面 Startup.cs。对于所有密集的目的,因为它涉及帮助OP获得他们正在寻找的东西,这是他们需要的,因为你把注意力称为“这是不可能的”。 - David Pine
@DavidPine这不是关于注册事项的位置,而是关于如何解决依赖关系的问题。引用自己的话说,“DI框架不可能神奇地将东西注入到该对象中”。 DI根本无法做到这一点。如果你选择一个完全不同的解决方案,这不是依赖注入和控制反转,当然你可以以某种方式使这项工作,但这不能使这与DI工作 - 它是一个完全不同的方法。 - poke
在我看来,这个答案非常好地总结了我的问题,以及为什么它无法通过设计获得解决方案。模型应该是简单的POCOS,完全没有依赖性。与它们相关的任何业务逻辑都应该在它们对应的repo中,或者在ModelController类中,就像MS Identity对UserStore一样。我不能将这个答案标记为解决方案,因为没有解决方案,所以我会给你的答案+1。 - kall2sollies


域驱动设计中常用的模式(富域模型是特定的)是将所需的服务传递给您调用的方法。

例如,如果您想计算增值税,您可以将增值税服务传递给 CalculateVat 方法。

在你的模型中

    public void CalculateVat(IVatCalculator vatCalc) 
    {
        if(vatCalc == null)
            throw new ArgumentNullException(nameof(vatCalc));

        decimal vatAmount = vatcalc.Calculate(this.TotalNetPrice, this.Country);
        this.VatAmount = new Currency(vatAmount, this.CurrencySymbol);
    }

你的服务类

    // where vatCalculator is an implementation IVatCalculator 
    order.CalculateVat(vatCalculator);

最后,您的服务可以注入其他服务,例如将获取某个国家/地区的税率的存储库

public class VatCalculator : IVatCalculator
{
    private readonly IVatRepository vatRepository;

    public VatCalculator(IVatRepository vatRepository)
    {
        if(vatRepository == null)
            throw new ArgumentNullException(nameof(vatRepository));

        this.vatRepository = vatRepository;
    }

    public decimal Calculate(decimal value, Country country) 
    {
        decimal vatRate = vatRepository.GetVatRateForCountry(country);

        return vatAmount = value * vatRate;
    }
}

4
2018-03-25 15:00



如果Entity Framework Code First知道如何处理IVatCalculator的注入,这实际上会起作用。它需要无参数构造函数。 - Milivoj Milani
@MilivojMilani:EF Core没有参与其中。实体不应该具有构造函数依赖项。从第一个例子可以看出计算器是 注入方法  模型,而不是它的构造函数。所以在您的订单服务中,您 明确地 呼叫 order.CalculateVat(vatCalculator); (order 作为你的域模型)并传递计算器的实例。所以EF / EFCore根本不参与这个过程 - Tseng


是否有另一种方式比构造函数注入注入依赖项,以及如何?

答案是“不”,这不能用“依赖注入”来完成。但是,“是”您可以使用“服务定位器模式”来实现您的最终目标。

您可以使用下面的代码来解决依赖关系,而无需使用构造函数注入或 FromServices 属性。另外你可以 new 你认为合适的类的实例,它仍然可以工作 - 假设你已经添加了依赖项 Startup.cs

public class MyRepository : IMyRepository
{
    public IMyDependency { get; } =
        CallContextServiceLocator.Locator
                                 .ServiceProvider
                                 .GetRequiredService<IMyDependency>();
}

CallContextServiceLocator.Locator.ServiceProvider 是一切生活的全球服务提供商。建议不要使用它。但如果你别无选择。建议改为使用 DI 一直以来都没有手动实例化一个对象,即;避免 new


2
2018-03-24 18:26



这实际上与本例中的OP代码没有任何区别,因为存储库也是通过DI注入的。 OP实际上是在问一个问题 模型 他们想要注入依赖的类。 - poke
正如之前的评论所述,[FromServices]是一个替代构造函数注入的类,它本身由DI解析。但是在问题评论中,@ poke指出了模型类是设计明确地实现的事实,这是我问题的根源。 - kall2sollies
这不是依赖注入,这是 服务定位器模式 是的 非常 不同于DI并且将类非常难以连接到静态类型,使得很难测试和击败控制反转的整个目的。 - poke
这个答案实际上提供了一个解决方案,但解决方案违反了设计规则(POCO应该最终只是POCO)并注入强大的耦合应该避免的模式。感谢您抽出时间,我无法将此答案标记为已被接受,但我已经+ 1-ed了。 - kall2sollies
@ kall2sollies:即使它老了,我刚才注意到了 [FromService] 评论。它不起作用。 [FromService] 只能在控制器动作中使用,因为rc2仅限于方法参数,因为人们混淆了它并试图将它用于通用方法/属性注入 - Tseng


内置模型绑定器抱怨他们找不到默认的ctor。因此,您需要一个自定义的。

您可以找到解决类似问题的方法 这里,检查注册的服务以创建模型。

值得注意的是,下面的代码段提供了稍微不同的功能,希望能够满足您的特定需求。下面的代码要求使用ctor注射模型。当然,这些模型具有您可能已定义的常用属性。这些属性完全按预期填充,所以奖金是 使用ctor注射绑定模型时的正确行为

    public class DiModelBinder : ComplexTypeModelBinder
    {
        public DiModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders) : base(propertyBinders)
        {
        }

        /// <summary>
        /// Creates the model with one (or more) injected service(s).
        /// </summary>
        /// <param name="bindingContext"></param>
        /// <returns></returns>
        protected override object CreateModel(ModelBindingContext bindingContext)
        {
            var services = bindingContext.HttpContext.RequestServices;
            var modelType = bindingContext.ModelType;
            var ctors = modelType.GetConstructors();
            foreach (var ctor in ctors)
            {
                var paramTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList();
                var parameters = paramTypes.Select(p => services.GetService(p)).ToArray();
                if (parameters.All(p => p != null))
                {
                    var model = ctor.Invoke(parameters);
                    return model;
                }
            }

            return null;
        }
    }

此活页夹将由以下人员提供:

public class DiModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }

        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
        {
            var propertyBinders = context.Metadata.Properties.ToDictionary(property => property, context.CreateBinder);
            return new DiModelBinder(propertyBinders);
        }

        return null;
    }
}

以下是绑定器的注册方式:

services.AddMvc().AddMvcOptions(options =>
{
    // replace ComplexTypeModelBinderProvider with its descendent - IoCModelBinderProvider
    var provider = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(ComplexTypeModelBinderProvider));
    var binderIndex = options.ModelBinderProviders.IndexOf(provider);
    options.ModelBinderProviders.Remove(provider);
    options.ModelBinderProviders.Insert(binderIndex, new DiModelBinderProvider());
});

我不太确定新的活页夹是否必须完全在同一索引上注册,你可以试试这个。

最后,这就是你如何使用它:

public class MyModel 
{
    private readonly IMyRepository repo;

    public MyModel(IMyRepository repo) 
    {
        this.repo = repo;
    }

    ... do whatever you want with your repo

    public string AProperty { get; set; }

    ... other properties here
}

模型类由提供(已注册)服务的活页夹创建,其余模型活页夹提供其常用来源的属性值。

HTH


1
2017-12-09 11:12





你可以这样做,查看[InjectionMethod]和container.BuildUp(instance);

例:

典型的DI构造函数(如果您使用InjectionMethod则不需要)公开   ClassConstructor(DeviceHead pDeviceHead){       this.DeviceHead = pDeviceHead; }

此属性导致调用此方法以设置DI。   [InjectionMethod] public void Initialize(DeviceHead pDeviceHead){       this.DeviceHead = pDeviceHead; }


0
2017-11-28 02:55



你能提供一个简单的代码片段,以便这个帖子的答案都是一致的吗? - kall2sollies
public ClassConstructor(DeviceHead pDeviceHead){this.DeviceHead = pDeviceHead; } [InjectionMethod] public void Initialize(DeviceHead pDeviceHead){this.DeviceHead = pDeviceHead; } - DougS