.NET Core DI,构造函数参数传递的方式

264

具有以下服务构造函数

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 >(),
        ""));

还有其他方法吗?


9
修改你的设计。将参数提取为一个参数对象,然后注入它。 - Steven
4个回答

320

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

使用它来解决依赖关系:

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

工厂代理是一种延迟调用。每当需要解析类型时,它将已完成的提供程序作为委托参数传递。


15
是的,这是我目前正在使用的方法,但是否还有其他方法?也许更加优雅一些?我的意思是,如果有其他已注册服务的参数,那就会显得有些奇怪。我正在寻找类似于通常注册服务并仅传递非服务参数(在此情况下为arg)的方法。就像Autofac中使用.WithParameter("argument", "");一样。 - user9124444
1
不,你正在手动构建提供程序,这是不好的。委托是延迟调用。每当需要解析类型时,它将已完成的提供程序作为委托参数传递。 - Nkosi
18
请查看ActivatorUtilities.CreateInstance方法,它是Microsoft.Extensions.DependencyInjection.Abstractions包的一部分(因此没有特定于容器的依赖)。 - Tseng
2
那么在解析时如何覆盖参数?并非每个参数都在注册时可用。类似于 https://docs.autofac.org/en/stable/resolve/parameters.html 的内容。 - Karel Kral
请查看 https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection 了解服务注册方法。 - Roshan
显示剩余2条评论

165
推荐实现这一点的方法是使用选项模式 - 请注意,这适用于任何.NET Core/5应用程序,而不仅仅是ASP.NET Core。但有些情况下这样做是不切实际的(例如当参数仅在运行时而不是在启动/编译时已知),或者您需要动态替换依赖项。
当您需要替换单个依赖项(无论是字符串、整数还是其他类型的依赖项)或者使用一个只接受字符串/整数参数并且需要运行时参数的第三方库时,它非常有用。
您可以尝试ActivatorUtilities.CreateInstance<T>(IServiceProvider, Object[])作为快捷方式,而不是手动解析每个依赖项:
_serviceCollection.AddSingleton<IService>(x => 
    ActivatorUtilities.CreateInstance<Service>(x, "");
);

传递给服务构造函数的参数( CreateInstance<T>/CreateInstanceobject[] 参数)允许您直接指定要注入的参数,而不是从服务提供程序中解析。它们按照它们出现的顺序从左到右应用(例如,第一个字符串将被替换为要实例化的类型的第一个字符串类型的参数)。ActivatorUtilities.CreateInstance<Service> 在许多地方用于解析服务并替换此单个激活的默认注册之一。例如,如果您有一个名为MyService的类,它具有IOtherServiceILogger<MyService> 作为依赖项,并且您想要解析服务但替换IOtherService的默认服务(假设是OtherServiceA),则可以像这样做:
myService = ActivatorUtilities.CreateInstance<Service>(serviceProvider,
    new OtherServiceB());

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

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

如果性能是问题,您可以使用ActivatorUtilities.CreateFactory(Type, Type[])创建工厂方法。 GitHub referencebenchmark

当该类型频繁解析时非常有用(例如在SignalR和其他高请求场景中)。基本上,您将通过创建一个ObjectFactory

var myServiceFactory = ActivatorUtilities.CreateFactory(typeof(MyService), new Type[] { 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", "Stackoverflow"));

        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 Stackoverflow

9
这也是 ASP.NET Core 默认实例化控制器的方式 ControllerActivatorProvider,它们不会直接从 IoC 容器中解析(除非使用了 .AddControllersAsServices,它将 ControllerActivatorProvider 替换为 ServiceBasedControllerActivator)。 - Tseng
6
值得注意的是,如果在依赖注入框架之外使用这个方法(本问题中的使用看起来没问题),会导致 服务定位器反模式 - Liam
4
дё»иҰҒзӣ®зҡ„жҳҜйҒҝе…ҚеңЁз»ҷе®ҡзұ»зҡ„жҜҸдёӘдҫқиө–йЎ№дёҠдҪҝз”Ёprovider.GetRequiredService<T>()пјҢеҗҢж—¶еҪ“жӮЁеҗ‘жһ„йҖ еҮҪж•°ж·»еҠ йҷ„еҠ еҸӮж•°ж—¶пјҢиҝҷд№ҹдјҡз ҙеқҸжӮЁзҡ„д»Јз ҒгҖӮ - Tseng
2
通常你会创建一些工厂,比如 MyServiceFactory,它会注入 IServiceProvider 并从那里解析(ActivatorUtils 需要 ServiceProvder 来解决依赖关系)。如果参数可以从服务中获取(例如 IHttpContextAccessor),则可以在注册时调用工厂方法:services.AddScoped<MyService>(provider => provider.GetRequiredService<MyServiceFactory>().Create()) - Tseng
2
@Benrobot:我已经告诉过你了,AddSingleton(s => new Service())AddSingleton(new Service())是有很大区别的。第一个实例化了一个Service实例并注册该实例应返回。另一方面,AddSingleton(sp => new Service())是一个工厂,它告诉.NET Core DI在需要时如何实例化它。当发生这种情况(调用工厂时),Microsoft.Extensions.DependecyInjection会检查对象是否实现了IDisposable,并将其注册为可处理的,在容器被处理时将被处理。 - Tseng
显示剩余4条评论

32

如果您对服务的新建感到不舒服,可以使用参数对象模式。

因此,将字符串参数提取为自己的类型。

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(...调用。另一个好处是设置更加简洁。

对于只有一个或两个参数的构造函数,这可能过于繁琐。


3
针对复杂参数,使用Options模式会更加直观,并且是选项模式的推荐方式,但对于仅在运行时(例如从请求或声明中获取)才知道的参数则不太适用。 - Tseng

6

你也可以使用这个过程注入依赖项

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接