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

102

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

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

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

борис
источник
3
Измени свой дизайн. Извлеките аргумент в объект параметра и введите его.
Стивен

Ответы:

121

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

Используйте это, чтобы разрешить зависимости,

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

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

Nkosi
источник
1
да, я так делаю сейчас, но есть ли другой способ? может быть, более элегантно? Я имею в виду, что было бы немного странно иметь другие параметры, которые являются зарегистрированными службами. Я ищу что-то большее, например, зарегистрировать службы в обычном режиме и передавать только аргументы, не связанные с обслуживанием, в данном случае arg. Что-то вроде Autofac .WithParameter("argument", "");
boris
1
Нет, вы создаете провайдера вручную, что плохо. Делегат - это отложенный вызов. Когда когда-либо тип должен быть разрешен, он передаст завершенный поставщик в качестве параметра делегата.
Nkosi
@MCR - это стандартный подход к Core DI из коробки.
Nkosi
11
@Nkosi: взгляните на ActivatorUtilities.CreateInstance , его часть Microsoft.Extensions.DependencyInjection.Abstractionsпакета (так что никаких зависимостей от контейнера)
Ценг
Спасибо, @Tseng, это похоже на настоящий ответ, который мы здесь ищем.
BrainSlugs83
59

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

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

Вы можете попробовать 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 []) Method для создания метода factory вместо этого, поскольку он обеспечивает лучшую производительность GitHub Reference и 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 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
Ценг
источник
6
Таким же образом ASP.NET Core создает экземпляры контроллеров по умолчанию ControllerActivatorProvider , они не разрешаются напрямую из IoC (если .AddControllersAsServicesне используется, который заменяет ControllerActivatorProviderсServiceBasedControllerActivator
Tseng
1
ActivatorUtilities.CreateInstance()именно то, что мне нужно. Благодарность!
Билли Джо
1
@Tseng Не могли бы вы просмотреть свой опубликованный код и опубликовать обновление. После создания расширений и классов верхнего уровня HellloWorldService я все еще сталкиваюсь с demoservice.HelloWorld как неопределенным. Я не понимаю, как это должно было работать достаточно, чтобы это исправить. Моя цель - понять, как работает этот механизм, так как мне это нужно.
Разработчик SOHO,
1
@SOHODeveloper: Очевидно, что public string HelloWorld()реализация метода отсутствовала
Ценг
Этот ответ более элегантный, и его следует принять ... Спасибо!
Exodium
15

Если вам стало неудобно при обновлении услуги, вы можете использовать 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>();

Первое преимущество заключается в том, что если вам нужно изменить конструктор службы и добавить к нему новые службы, вам не нужно изменять new Service(...вызовы. Еще одно преимущество - установка немного чище.

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

Адриан Ифтоде
источник
2
Для сложных параметров было бы более интуитивно понятно использовать шаблон параметров и это рекомендуемый способ для шаблона параметров, однако он менее подходит для параметров, известных только во время выполнения (то есть из запроса или утверждения)
Tseng