.NET Core DI, способы передачи параметров в конструктор

Имея следующий сервисный конструктор

public class Service : IService
{
     public Service(IOtherService service1, IAnotherOne service2, string arg)
     {

     }
}

Каковы варианты передачи параметров с использованием механизма .NET Core IOC

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

Есть ли другой путь?

Ответ 1

Параметр выражения (в данном случае x) фабричного делегата является IServiceProvider.

Используйте это для разрешения зависимостей,

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

Фабричный делегат является отложенным вызовом. Когда тип должен быть разрешен, он передает завершенного поставщика в качестве параметра делегата.

Ответ 2

Следует отметить, что рекомендуемым способом является использование шаблона параметров. Но есть случаи использования, когда это непрактично (когда параметры известны только во время выполнения, а не во время запуска/компиляции) или вам нужно динамически заменить зависимость.

Это очень полезно, когда вам нужно заменить одну зависимость (будь то строка, целое число или другой тип зависимости) или когда используется сторонняя библиотека, которая принимает только строковые/целочисленные параметры, и вам требуется параметр времени выполнения.

Вы можете попробовать CreateInstance (IServiceProvider, Object []) в качестве ярлыка (не уверен, что он работает со строковыми параметрами/типами значений/примитивами (int, float, string), без проверки) (Простоопробовал его и подтвердил его работу, даже с несколькими строковыми параметрами) вместо того, чтобы разрешать каждую зависимость вручную:

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

Параметры (последний параметр CreateInstance<T>/CreateInstance) определяют параметры, которые должны быть заменены (не разрешены поставщиком). Они применяются слева направо по мере их появления (т.е. первая строка будет заменена первым строковым параметром того типа, который будет создан).

ActivatorUtilities.CreateInstance<Service> используется во многих местах для разрешения службы и замены одной из регистраций по умолчанию для этой отдельной активации.

Например, если у вас есть класс с именем MyService, и он имеет IOtherService, ILogger<MyService> в качестве зависимостей, и вы хотите разрешить службу, но замените службу по умолчанию IOtherService (скажем, OtherServiceA) на OtherServiceB, вы можете сделать что-то вроде:

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

Тогда первый параметр IOtherService будет получать OtherServiceB, а не OtherServiceA, но остальные параметры будут поступать из контейнера.

Это полезно, когда у вас много зависимостей и вы хотите просто специально обработать одну (т.е. заменить поставщика данных базы данных значением, настроенным во время запроса или для конкретного пользователя), то, что вы знаете только во время выполнения и во время запроса и не когда приложение построено/запущено).

Вы также можете использовать ActivatorUtilities.CreateFactory(Type, Type []) метод, чтобы создать метод фабрики, поскольку он предлагает лучшую производительность Справочник по GitHub и Benchmark.

Позже это полезно, когда тип разрешается очень часто (например, в SignalR и других сценариях с высокими запросами). По сути, вы бы создали ObjectFactory через

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

Ответ 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(... вызовы. Другое преимущество заключается в том, что настройка немного чище).

Для конструктора с одним или двумя параметрами это может быть слишком много.

Ответ 4

Следующий способ будет работать, даже если у вас есть несколько параметров

services.AddSingleton(_ => new YourService("yourvalue1","yourvalue2") as IYourService);

Но я чувствую его неэффективный способ реализации