【问题标题】:Property Injection in ASP.NET CoreASP.NET Core 中的属性注入
【发布时间】:2016-07-19 13:10:21
【问题描述】:

我正在尝试将 ASP.NET 应用程序移植到 ASP.NET Core。我在我的 UnitOfWork 实现上进行了属性注入(使用 Ninject),如下所示:

[Inject]
public IOrderRepository OrderRepository { get; set; }
[Inject]
public ICustomerRepository CustomerRepository { get; set; }

有没有办法在 .NET Core 上使用内置 DI 来实现相同的功能?另外,是否可以使用基于约定的绑定?

【问题讨论】:

标签: dependency-injection asp.net-core


【解决方案1】:

不,内置的 DI/IoC 容器在使用和功能上都刻意保持简单,以便为其他 DI 容器插件提供基础。

因此没有内置支持:自动发现、自动注册、装饰器或注入器,或基于约定的注册。据我所知,目前还没有计划将其添加到内置容器中。

您必须使用支持属性注入的第三方容器。

请注意,属性注入在 98% 的情况下都被认为是不好的,因为它隐藏了依赖关系,并且不能保证在创建类时会注入对象。

使用构造函数注入,您可以通过构造函数强制执行此操作并检查 null 并且不创建类的实例。使用属性注入这是不可能的,并且在单元测试期间,当未在构造函数中定义类需要哪些服务/依赖项时,它们并不明显,因此很容易错过并获得NullReferenceExceptions

我发现进行属性注入的唯一正当理由是将服务注入到由第三方库生成的代理类中,即从您无法控制对象创建的接口创建的 WCF 代理。即使在那里,它也仅适用于第三方库。如果您自己生成 WCF 代理,您可以通过 partial class 轻松扩展代理类并添加新的 DI 友好的构造函数、方法或属性。

在其他地方在任何地方避免它。

【讨论】:

  • 您是否希望我有十几个构造函数参数,并且还要花费另外十几个初始化代码行,所有这些都是为了避免一些无论如何都不会发生的“NRE”(如果编码正确)?
  • @AgentFire:它不是关于编写一些额外的行(顺便说一句。可以通过重构助手完成 90%,即创建、初始化和 nullcheck 构造函数参数),它也不利于编写单元测试(因为依赖项变得不明显,测试一个类的人不会知道他/她需要提供哪个依赖项,或者如果它隐藏在属性后面,则模拟它,而在构造函数上它非常明显)。最重要的是,当一个类做太多事情时,构造函数注入很明显(当你有超过 4-5 个依赖项时,这意味着你的抽象很糟糕并且类试图做太多事情)
  • @AgentFire:不,不是。当您执行var sut = new MyDomainService() 时,很明显依赖关系是什么,尤其是如果类位于不同的库中(其源代码可能会或可能不会被使用它的团队访问)。但是,var sut = new OrderService(orderRepository, customerRepository) 因为您必须将它传递给构造函数,所以您会立即看到它(类不能实例化 w/o)。另外var sut = new MyService(dep1,dep2, dep3, dep4, dep5, dep6, ... dep10) 也让你立刻变得明显,你的课程做了很多事情,你需要重构它
  • @Tseng 无需闷闷不乐先生'大型关键业务应用程序',我们都写这些。我在公司的项目中使用属性注入作为一种有利的方法,以避免必然会发生很大变化的样板。正确配置后,好的 DI 框架应该为您执行空值检查。您唯一需要适应的是不要使用构造函数显式创建服务实例。与编写带有 5 个参数的构造函数相比,学习这一点花费的时间更少。
  • @Tseng Ctor 注入要求您将每个名称重复四次。这只是意识形态。每隔 10 年左右,其中一种时尚就会席卷整个行业,并迫使每个人都输入毫无意义的样板文件,以便在工具赶上并再次隐藏混乱之前,一些傻瓜会觉得很重要。上次是 Java 中的 setter 和 getter,本来应该在一行上的东西被膨胀到半屏。
【解决方案2】:

有没有办法使用 .NET Core 上的内置 DI 实现相同的功能?

不,但是您可以在 Autofac's property injection mechanism 的帮助下创建自己的 [inject] 属性。

首先创建你自己的InjectAttribute

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class InjectAttribute : Attribute
{
  public InjectAttribute() : base() { }
}

然后创建您自己的InjectPropertySelector,它使用反射来检查标有[inject] 的属性:

public class InjectPropertySelector : DefaultPropertySelector
{
  public InjectPropertySelector(bool preserveSetValues) : base(preserveSetValues)
  { }

  public override bool InjectProperty(PropertyInfo propertyInfo, object instance)
  {
    var attr = propertyInfo.GetCustomAttribute<InjectAttribute>(inherit: true);
    return attr != null && propertyInfo.CanWrite
            && (!PreserveSetValues
            || (propertyInfo.CanRead && propertyInfo.GetValue(instance, null) == null));
  }
}

然后在您的ConfigureServices where you wire up 您的AutofacServiceProvider 中使用您的选择器:

public class Startup
{
  public IServiceProvider ConfigureServices(IServiceCollection services)
  {
    var builder = new ContainerBuilder();
    builder.Populate(services);
    
    // use your property selector to discover the properties marked with [inject]
    builder.RegisterType<MyServiceX>().PropertiesAutowired((new InjectablePropertySelector(true)););

    this.ApplicationContainer = builder.Build();
    return new AutofacServiceProvider(this.ApplicationContainer);
  }
}

终于在您的服务中,您现在可以使用[inject]

public class MyServiceX 
{
    [Inject]
    public IOrderRepository OrderRepository { get; set; }
    [Inject]
    public ICustomerRepository CustomerRepository { get; set; }
}

您当然可以更进一步地采用此解决方案,例如通过在服务的类定义之上使用属性来指定服务的生命周期...

[Injectable(LifetimeScope.SingleInstance)]
public class IOrderRepository

...然后在通过 Autofac 配置服务时检查此属性。但这将超出此答案的范围。

【讨论】:

    【解决方案3】:

    票数最高的答案并不完全正确。虽然内置 DI 容器 (IServiceProvider) 特意保持简单且功能简单,但它的可扩展性也很强,并且可以通过一些管道提供您需要的功能。

    这就是Quickwire NuGet 包提供的开箱即用的功能。这正如你所期望的那样工作。

    首先用[RegisterService]装饰类,使用[InjectService]属性注入的属性:

    [RegisterService(ServiceLifetime.Scoped)]
    public class BusinessLogic
    {
        [InjectService]
        public IOrderRepository OrderRepository { get; set; }
        [InjectService]
        public ICustomerRepository CustomerRepository { get; set; }
    
        // ...
    }
    

    然后从ConfigureServices使用ScanCurrentAssembly

    public void ConfigureServices(IServiceCollection services)
    {
        services.ScanCurrentAssembly();
    
        // Register other services...
    }
    

    这将扫描当前程序集以查找以RegisterService 修饰的类并将它们注册到IServiceCollection,同时确保将依赖项注入以[InjectService] 修饰的setter。

    再一次,这种方法不需要您使用像 Autofac 这样的第三方依赖注入容器。您现有的所有代码都将继续工作,无需任何修改。

    【讨论】:

      【解决方案4】:

      我不想争论,因为我是 .NET Core 的新手。 但是想象这样一种情况:class A 使用服务 Service1, ..., Service5 并且它们的接口 IService1, ..., IService5 在其构造函数中作为依赖项传递; class B 继承自 class Aclass C 继承自 class B 等,因此所有这些派生类都应在其构造函数中至少传递 IService1, ..., IService5 接口。但是,class B 和从它派生的所有类都放置在消费者库或应用程序中,因此超出了class A 开发人员的范围。

      好吧,现在class A 的开发人员会发现,合并到class A 中的某些功能值得重构和外包到外部服务中,因为它需要在其他地方使用,而class A 无法触及。所以它将其外包给带有接口 IService6Service6。但是通过向class A 构造函数添加另一个参数,开发人员违反了与此类用户的约定:构造函数用于派生类,并且只知道它的 5 个参数。

      解决方案?或者:

      1. 通过属性注入IService6

      2. 而不是将单个服务的 5 个接口传递给构造函数,而是传递一个接口,它将在其属性中为上述 5 个服务提供 5 个接口。此接口将成为class A 随附的软件包的一部分。如果需要将任何class A 功能外包给服务,则会将另一个属性添加到此接口,并将另一个服务添加到捆绑包中。所有class A的依赖(可能除了options)都会在这个界面中清晰的总结出来。

      3. 有没有更好的解决方案?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-04-14
        • 2019-03-18
        • 1970-01-01
        • 1970-01-01
        • 2017-11-12
        • 2019-05-27
        • 2018-01-16
        • 2019-02-22
        相关资源
        最近更新 更多