Лучшая стратегия для создания дочернего контейнера (или изолированной области) с Microsoft.Extensions.DependencyInjection - PullRequest
1 голос
/ 21 апреля 2020

В моем приложении AspNetCore я обрабатываю сообщения, поступающие из очереди. Чтобы обработать сообщение, мне нужно разрешить некоторые службы. Некоторые из этих служб имеют зависимость от ITenantId, которую я связываю, используя информацию из полученного сообщения. Чтобы решить эту проблему, обработка сообщений начинается с создания дочернего контейнера, который я затем использую для создания IServiceScope , из которого я разрешаю все необходимые зависимости.

Сообщения могут обрабатываться параллельно, поэтому области должны быть изолированы друг от друга.

Я вижу способы создания дочернего контейнера, но я не уверен, что лучше с точки зрения производительности, память chrurn et c:

Вариант A: Каждый раз, когда приходит сообщение, клонируйте коллекцию IServiceCollection в новую коллекцию ServiceCollection и повторно привязывайте ITenantId в клонированном экземпляре.

Вариант B: при запуске программы создайте неизменную копию коллекции IServiceCollection (используя ImmutableList<ServiceDescriptor> или ImmutableArray<ServiceDescriptor>). Каждый раз, когда приходит сообщение, замените ITenantId (что приводит к новому экземпляру ImmutableList<ServiceDescriptor>) и вызовите CreateScope() для нового неизменяемого экземпляра.

В опции A мне не нравится то, что Вся коллекция услуг должна быть клонирована каждый раз, когда приходит сообщение. Я не уверен, что неизменные коллекции в варианте B обрабатывают это более разумно?

1 Ответ

1 голос
/ 21 апреля 2020

Обе опции вызывают создание нового экземпляра контейнера для каждого входящего сообщения. Хотя это позволяет каждому сообщению работать в полностью изолированном пузыре, это имеет серьезные последствия для производительности и использования памяти приложением. Создание экземпляров контейнера стоит дорого, и разрешение зарегистрированного экземпляра в первый раз (для каждого контейнера) вызывает генерацию деревьев выражений, компиляцию делегатов и JIT-компиляцию их на диск. Это может даже вызвать утечку памяти.

Кроме того, это также означает, что любой зарегистрированный одноэлементный класс будет иметь время жизни, равное времени жизни любых классов с областью действия. Состояние больше не может быть общим.

Поэтому вместо этого я предлагаю вариант 3:

  • Использовать только один экземпляр контейнера и не вызывать BuildProvider более одного раза
  • Создайте реализацию ITenantId, которая позволяет установить Id после создания экземпляра
  • Зарегистрируйте эту реализацию как Scoped
  • В начале каждого нового IServiceScope, решите, что реализации и установить его идентификатор.

Это может выглядеть следующим образом:

// Code
class TenentIdImpl : ITenantId
{
    public Guid Id { get; set; } // settable
}

// Startup:
services.AddScoped<TenentIdImpl>();
services.AddScoped<ITenantId>(c => c.GetRequiredService<TenantIdImpl>());

// In message pipeline
using (var scope = provider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
    var tenant = scope.ServiceProvider.GetRequiredService<TenantIdImpl>();
    tenant.Id = messageEnvelope.TenantId;

    var handler = scope.ServiceProvider.GetRequiredService<IMessageHandler<TMessage>>();

    handler.Handle(messageEnvelope.Message);
}

Эта конкретная модель, где вы храните состояние внутри графа объекта, который я объясняю в моем блоге, называется Композиционная модель замыкания .

...