.NET Core DI,将参数传递给构造函数的方法

Posted

技术标签:

【中文标题】.NET Core DI,将参数传递给构造函数的方法【英文标题】:.NET Core DI, ways of passing parameters to constructor 【发布时间】:2019-05-21 21:42:15 【问题描述】:

具有以下服务构造函数

public class Service : IService

     public Service(IOtherService service1, IAnotherOne service2, string arg)
         
     

使用.NET Core IOC机制传递参数有哪些选择

services.AddSingleton<IOtherService , OtherService>();
services.AddSingleton<IAnotherOne , AnotherOne>();
services.AddSingleton<IService>(x =>
    new Service(
        services.BuildServiceProvider().GetService<IOtherService>(),
        services.BuildServiceProvider().GetService<IAnotherOne >(),
        ""));

还有其他方法吗?

【问题讨论】:

改变你的设计。将 arg 提取到参数对象中并注入。 【参考方案1】:

工厂委托的表达式参数(本例中为x)是IServiceProvider

使用它来解决依赖关系:

_serviceCollection.AddSingleton<IService>(x => 
    new Service(x.GetRequiredService<IOtherService>(),
                x.GetRequiredService<IAnotherOne>(), 
                ""));

工厂委托是延迟调用。每当要解析类型时,它都会将完成的提供者作为委托参数传递。

【讨论】:

是的,这就是我现在正在做的方式,但还有其他方式吗?也许更优雅?我的意思是其他参数是注册服务看起来有点奇怪。我正在寻找更像是正常注册服务并且只传递非服务参数的东西,在这种情况下是arg。像 Autofac 这样的东西 .WithParameter("argument", ""); 不,您正在手动构建提供程序,这很糟糕。委托是延迟调用。每当要解析类型时,它将完整的提供者作为委托参数传递。 @Nkosi:看看ActivatorUtilities.CreateInstance,它是Microsoft.Extensions.DependencyInjection.Abstractions 包的一部分(因此没有容器特定的依赖项)【参考方案2】:

实现此目的的推荐方法是使用Options pattern - 请注意,这适用于任何 .NET Core/5 应用程序,而不仅仅是 ASP.NET Core。但是在某些用例中它是不切实际的(例如,当参数仅在运行时知道,而不是在启动/编译时知道时)或者您需要动态替换依赖项。

当您需要替换单个依赖项(无论是字符串、整数或其他类型的依赖项)或使用仅接受字符串/整数参数且需要运行时参数的 3rd 方库时,它非常有用。

您可以尝试CreateInstance&lt;T&gt;(IServiceProvider, Object[]) 作为快捷方式,而不是手动解决每个依赖项:

_serviceCollection.AddSingleton<IService>(x => 
    ActivatorUtilities.CreateInstance<Service>(x, "");
);

传递给服务构造函数的参数(object[] 参数传递给CreateInstance&lt;T&gt;/CreateInstance)允许您指定应直接注入的参数,而不是从服务提供者解析。它们在出现时从左到右应用(即第一个字符串将替换为要实例化的类型的第一个字符串类型参数)。

ActivatorUtilities.CreateInstance&lt;Service&gt; 在许多地方用于解析服务并替换此单一激活的默认注册之一。

例如,如果您有一个名为MyService 的类,并且它具有IOtherServiceILogger&lt;MyService&gt; 作为依赖项,并且您想要解析服务但替换默认服务IOtherService(比如说它是OtherServiceA ) 使用OtherServiceB,您可以执行以下操作:

myService = ActivatorUtilities.CreateInstance<Service>(serviceProvider,
    new OtherServiceB());

那么IOtherService 的第一个参数将被OtherServiceB 注入,而不是OtherServiceA - 但其余参数将来自服务提供者。

当您有许多依赖项并且只想特别对待一个依赖项时,这很有帮助(即用请求期间或特定用户配置的值替换特定于数据库的提供程序,这是您仅在运行时知道的内容和/或在请求期间 - 而不是在构建/启动应用程序时)。

如果关注性能,您可以使用ActivatorUtilities.CreateFactory(Type, Type[]) 创建工厂方法。 GitHub reference 和 benchmark。

当类型被非常频繁地解析时(例如在 SignalR 和其他高请求场景中),这很有用。基本上,你会通过

创建一个ObjectFactory
var myServiceFactory = ActivatorUtilities.CreateFactory(typeof(MyService), new object[]  typeof(IOtherService), );

然后将其缓存(作为变量等)并在需要时调用它:

MyService myService = myServiceFactory(serviceProvider, myServiceOrParameterTypeToReplace);

这一切也适用于原始类型 - 这是我测试过的一个示例:

class Program

    static void Main(string[] args)
    
        var services = new ServiceCollection();
        services.AddTransient<HelloWorldService>();
        services.AddTransient(p => p.ResolveWith<DemoService>("Tseng", "***"));

        var provider = services.BuildServiceProvider();

        var demoService = provider.GetRequiredService<DemoService>();

        Console.WriteLine($"Output: demoService.HelloWorld()");
        Console.ReadKey();
    


public class DemoService

    private readonly HelloWorldService helloWorldService;
    private readonly string firstname;
    private readonly string lastname;

    public DemoService(HelloWorldService helloWorldService, string firstname, string lastname)
    
        this.helloWorldService = helloWorldService ?? throw new ArgumentNullException(nameof(helloWorldService));
        this.firstname = firstname ?? throw new ArgumentNullException(nameof(firstname));
        this.lastname = lastname ?? throw new ArgumentNullException(nameof(lastname));
    

    public string HelloWorld()
    
        return this.helloWorldService.Hello(firstname, lastname);
    


public class HelloWorldService

    public string Hello(string name) => $"Hello name";
    public string Hello(string firstname, string lastname) => $"Hello firstname lastname";


// Just a helper method to shorten code registration code
static class ServiceProviderExtensions

    public static T ResolveWith<T>(this IServiceProvider provider, params object[] parameters) where T : class => 
        ActivatorUtilities.CreateInstance<T>(provider, parameters);

打印

Output: Hello Tseng ***

【讨论】:

这也是 ASP.NET Core 默认实例化控制器 ControllerActivatorProvider 的方式,它们不是直接从 IoC 解析的(除非使用 .AddControllersAsServices,它将 ControllerActivatorProvider 替换为 @987654352 @ 值得注意的是,使用这个在 DI 框架之外(这个问题中的用法对我来说看起来不错)将是 service locator anti-pattern。 @Wouter:不,你不能。有问题的构造函数有 3 个构造函数参数。当您new 它时,您需要传递每个构造函数参数。使用 ActivatorUtils,您只传递不应来自容器的参数。当 string/int 参数是在运行时而不是在启动时确定时(这是配置 DI 容器并且之后无法更改),这尤其有用。该代码用于演示,以表明您可以注入运行时参数(甚至是服务实例),而其余的来自 DI 容器 重点是避免给定类的每个依赖项都必须provider.GetRequiredService&lt;T&gt;(),当您向构造函数添加附加参数时,这也会破坏您的代码 这最后一个程序为您提供服务。AddTransient(p => p.ResolveWith("Tseng", "***"));似乎毫无意义,因为您可以只使用 services.AddTransient(sp => new DemoService(p.GetRequiredService(), "Tseng", "***"))【参考方案3】:

如果您对新服务感到不舒服,可以使用Parameter Object 模式。

所以把字符串参数提取成自己的类型

public class ServiceArgs

   public string Arg1 get; set;

构造函数现在看起来像

public Service(IOtherService service1, 
               IAnotherOne service2, 
               ServiceArgs args)



还有设置

_serviceCollection.AddSingleton<ServiceArgs>(_ => new ServiceArgs  Arg1 = ""; );
_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService, Service>();

第一个好处是,如果您需要更改 Service 构造函数并向其添加新服务,则无需更改 new Service(... 调用。另一个好处是设置更干净。

对于一个或两个参数的构造函数,这可能太多了。

【讨论】:

复杂参数使用Options pattern会更直观,是选项模式的推荐方式,但不太适合仅在运行时知道的参数(即来自请求或声明) 【参考方案4】:

您也可以通过此过程注入依赖项

_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService>(x=>new Service( x.GetService<IOtherService>(), x.GetService<IAnotherOne >(), "" ));

【讨论】:

以上是关于.NET Core DI,将参数传递给构造函数的方法的主要内容,如果未能解决你的问题,请参考以下文章

将数组作为命令行参数传递给 Asp.Net Core

如何将多个参数传递给 ASP.NET CORE MVC 中的 HttpGet 方法?

在 ASP.Net Core 项目中使用 ADO.Net 将 JSON 类型作为参数传递给 SQL Server 2016 存储过程

如何将参数传递给静态类构造函数?

当我们将对象作为参数传递给方法时,为啥会调用复制构造函数?

将参数传递给超类构造函数