Dependency Injection и Message Broker

У меня есть собственная реализация брокера сообщений (по типу MessagePipe), там все тоже реализовано через DI и использование интерфейсов типа IReceiver<T> (для получения), ISender<T> (для отправки).

Я верно понимаю, что до тех пор пока не будет нужды в экземпляре класса, то DI даже не дернется, чтобы создать экземпляр, а следовательно все содержащиеся ISender, IReceiver не будут активны до востребования.

Как обойти это? Просто мне было интересно реализовать приложение, которое работает полностью через события. Есть конечно дешево-сердитый вариант - собственноручно запрашивать экземпляры через GetRequiredService<T>() после сборки контейнера. Мне в голову приходит еще вариант добавить к таким сервисам какой-нибудь интерфейс и уже по интерфейсу запрашивать все классы, но адекватно ли это?


Ответы (1 шт):

Автор решения: d1rknwh1te3

Я просмотрел разную информацию и нашел несколько вариантов, некоторое были описаны тут, до некоторых докопался. Я опишу их ниже, может кому-то это пригодится в будущем, кто столкнется с такой же проблемой.

Переформулирую проблему тут, потому что в вопросе я немного неясно выразился. Суть такова: Как активировать все классы, которые содержат брокеры, чтобы на старте программы они могли обмениваться сообщениями? Какой способ наиболее оптимальный?

По стандарту DI-контейнеры используют ленивую инициализацию (lazy loading), т.е. объект создается только в тот момент, когда он потребуется. Так банально эффективнее, но в моем случае классы явно не связаны друг с другом, поэтому момент, когда они потребуются не наступит, а следовательно не будут и созданы подписки. Поэтому нужно как-то автоматически активировать нужные классы.

  1. Использовать авто-активацию через Microsoft.Extensions.DependencyInjection.AutoActivation (или Autofac) (мой выбор)

Ниже будет пример кода для Microsoft, узнал я о таком функционале вот здесь -> issue. Я немного удивлен, что это было реализовано относительно недавно в .NET 8.0.

builder.Services.AddActivatedSingleton<T>();

Для Autofac все еще проще и поддерживается из коробки. Подробнее тут -> документация.

containerBuilder.RegisterType<T>()
    .AsSelf()
    .AutoActivate();
  1. Использовать IHostedService (или IStartable)

Еще можно сделать каждый сервис, как IHostedService, тогда будет вызван метод StartAsync, в котором должна будет выполняться бесконечно долгая задача по типу:

while(!cts.IsCancellationRequested)
{
   ...
}

В противном случае Task StartAsync завершится и сервис будет остановлен. Еще я не уверен, что будет, если один IHostedService зависит от другого. Решает ли что-то в этом случае Microsoft DI.

IStartable из Autofac. Здесь делать бесконечно долгую задачу не надо. Надо просто реализовать интерфейс IStartable в нужных сервисах. В случае зависимостей между IStartable Autofac гарантирует, что метод Start зависимости будет вызван раньше, чем у зависящего класса.

  1. Инициализация ручками

Это самый простой способ. У нас по любому есть общая точка, которая 100% будет существовать. Поэтому нужные классы можно вызвать ручками. Первый способ - это во время запуска приложения. Второй способ - в любом классе программы.

private static IServiceCollection Instantiate(this IServiceCollection services) // Mircosoft.Extensions.DependencyInjection
{
    using var provider = services.BuildServiceProvider();
        
    _ = provider.GetRequiredService<T>(); // для каждого класса
        
    return services;
}
private static ContainerBuilder Instantiate(this ContainerBuilder builder) // Autofac
{
    using var container = builder.Build();
        
    _ = container.Resolve<T>(); // для каждого класса
        
    return builder;
}

Второй способ - это любая точка внедрения, куда сможет это сделать DI.

public Foo(Boo boo, Goo goo, Loo loo) { ... } // через конструктор
public void Foo(Boo boo, Goo goo, Loo loo) { ... } // через метод

И через иные точки внедрения зависимости: атрибуты, свойства и так далее...

  1. Через атрибут

Нужно сделать атрибут, условно AutoActivate и при помощи инструментов, таких как Scrutor можно делать это как-то вроде так.

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class AutoActivateAttribute : Attribute
{
   ...
}
public static IServiceCollection AddAutoActivateServices(this IServiceCollection services)
{
    services.Scan(scan => scan
        .FromAssemblyOf<Program>()
        .AddClasses(classes => classes.WithAttribute<AutoActivateAttribute>())
        .UsingRegistrationStrategy(RegistrationStrategy.Skip)
        .AsSelfWithInterfaces()
    );

    using var provider = services.BuildServiceProvider()

    _ = GetRequiredService<T>();
        
    return services;
}
  1. Через общий интерфейс

Тоже самое, но только ищем по интерфейсу, затем получаем просто IEnumerable<Наш_интерфейс> и дело готово.

  1. Через атрибут + интерфейс

Тут вариант в виде комбинации двух подходов выше (4 и 5). Я описывать его не буду, потому что всё, начиная с 4 это буквально самостоятельная реализация велосипеда. И как бы раз я уже нашел хороший способ, то на нем и поеду.

→ Ссылка