.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

Параметр виразу ( х - у цьому випадку) заводського делегата - a IServiceProvider.

Використовуйте це для вирішення залежностей,

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

Делегат заводу є відкладеним викликом. Коли тип буде вирішено, він передаватиме завершеного постачальника як параметр делегата.


1
так, саме зараз я це роблю, але чи є інший спосіб? може, більш елегантний? Я маю на увазі, здавалося б трохи дивним наявність інших параметрів, які є зареєстрованими послугами. Я шукаю щось більше на зразок нормальної реєстрації служб і передаю лише несервісні аргументи, в даному випадку аргумент. Щось подібне Autofac робить .WithParameter("argument", "");
борис

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 (Тип, Тип []) для створення заводського методу, оскільки він пропонує кращу продуктивність 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 Ядро инициализирует контролерів за замовчуванням ControllerActivatorProvider , вони безпосередньо не дозволені з IoC (якщо .AddControllersAsServicesне використовується, який замінює ControllerActivatorProviderзServiceBasedControllerActivator
Цзен

1
ActivatorUtilities.CreateInstance()саме те, що мені потрібно. Дякую!
Біллі Джо,

1
@Tseng Будьте люб'язними, щоб переглянути свій розміщений код та опублікувати оновлення. Зробивши розширення та класи найвищого рівня HellloWorldService, я все ще стикаюся з demoservice.HelloWorld як невизначений. Я не розумію, як це мало працювати настільки, щоб це виправити. Моя мета - зрозуміти, як працює цей механізм, як мені це потрібно.
розробник SOHO

1
@SOHODeveloper: Ну, очевидно, реалізація public string HelloWorld()методу відсутня
Ценг

Ця відповідь більш елегантна і її слід прийняти ... Дякую!
Екзодіум

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
Для складних параметрів було б більш інтуїтивно зрозумілим використовувати шаблон Опції, і це рекомендований спосіб для шаблону опцій, однак менш придатний для параметрів, відомих лише під час виконання (тобто з запиту чи претензії)
Ценг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.