'Best strategy for creating a child container (or isolated scope) with Microsoft.Extensions.DependencyInjection

In my AspNetCore application, I process messages that arrive from a queue. In order to process a message, I need to resolve some services. Some of those services have a dependency on ITenantId, which I bind using information from the received message. To solve this, the processing of a messages starts with the creation of a child container, which I then use to create an IServiceScope from which I resolve all the needed dependencies.

The messages can be processed in parallel, so the scopes must be isolated from each other.

I can see to ways of creating the child container, but I'm not sure which is best in terms of performance, memory chrurn etc:

Option A: Each time a message arrives, clone the IServiceCollection into a new ServiceCollection, and rebind ITenantId on the cloned instance.

Option B: When the program starts, create an immutable copy of the IServiceCollection (using ImmutableList<ServiceDescriptor> or ImmutableArray<ServiceDescriptor>). Each time a message arrives, replace ITenantId (resulting in a new instance of ImmutableList<ServiceDescriptor>) and call CreateScope() on the new immutable instance.

The thing I don't like about option A is that the whole collection of services needs to be cloned every time a message arrives. I'm not sure if the immutable collections in option B handles this in a smarter way?



Solution 1:[1]

Both options cause the creation of a new container instance for each incoming messages. Although this allows each message to run in a completely isolated bubble, this has severe implications on performance and memory use of the application. Creating container instances is expensive and resolving a registered instance for the first time (per container) causes generation of expression trees, compilation of delegates, and JIT compiling them. This can even cause memory leaks.

Besides, it also means that any registered singleton class, will have a lifetime that equals that of any scoped classes. State can't be shared any longer.

So instead, I propose Option 3:

  • Use only one Container instance and don't call BuildProvider more than once
  • Create a ITenantId implementation that allows setting the Id after instantiation
  • Register that implementation as Scoped
  • At the start of every new IServiceScope, resolve that implementation and set its id.

This might look as follows:

// 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.CreateScope())
{
    var tenant = scope.ServiceProvider.GetRequiredService<TenantIdImpl>();
    tenant.Id = messageEnvelope.TenantId;

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

    handler.Handle(messageEnvelope.Message);
}

This particular model, where you store state inside your object graph, which I explain in my blog, is called the Closure Composition Model.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1