Dynamic scoped dependency resolution in a console app for multiple tenants using dependency injection

Viewed 219

In case of a Web API, each request is a distinct scope and dependencies registered as scoped will get resolved per request. So resolving dependencies per request per tenant is easy as the tenant information (like TenantId) can be passed in the HTTP Request headers like below:

services.TryAddScoped<ITenantContext>(x =>
{
    var context = x.GetService<IHttpContextAccessor>().HttpContext;
    var tenantId = context.Request.Headers["TenantId"].ToString();
    var tenantContext = GetTenantContext(tenantId);
    return tenantContext;
}

Other registrations first resolve TenantContext and use it to resolve other dependencies. For example, IDatabase will be registered as below. During resolution it will resolve and connect to specific tenant database.

services.TryAddScoped<IDatabase>(x =>
{
    var tenantContext = x.GetService<ITenantContext>();
    return new Database(tenantContext.DatabaseConnectionString);
}

This is all good in a Web API service because each request is a scope. I am facing challenges using dependency injection in a multi-tenant Console App. Suppose the app processes items from a multi-tenant queue and each message can belong to a different tenant. While processing each message, it commits data to tenant specific database. So in this case the scope is each message in a queue and message contains the tenantId.

So when the app reads a message from queue, it needs to get TenantContext. Then resolve other dependencies based on this TenantContext.

One straightforward option I see how this dynamic resolution can be achieved is to create the dependent objects manually using the TenantContext but then I wouldn't be able to leverage dependency injection. All objects would get created manually and disposed after going out of scope after the message is processed.

var messgage = GetMessageFromQueue(queueName);
var tenantContext = GetTenantContext(message.TenantId);
var database = GetDatabaseObject(tenantContext);
// Do other processing now we got the database object connected to specific tenant DB

Is there an option in DI where I can pass in the TenantId dynamically so that TenantContext gets set for this scope and then all further resolution within this scope leverage this TenantContext?

1 Answers

Because the role of the tenancy goes beyond the implementation ("this uses X database") and is actually contextual to the action being performed ("this uses X database and must use this connection string based on the context being handled in the action"), there's some risk of assuming that ambient context is present in alternate implementations due to it not expressly being described in your interface in some way, which is where the DI issue is coming up here.

You might be able to:

  1. Update your interfaces so that the tenancy information is an expected parameter of your methods. This ensures that regardless of future implementation, the presence of the tenant ID is explicit in their signature:

    public interface ITenantDatabase {
       public TResponse Get(string TenantId, int Id);
       //... other methods ...
    }
    
  2. Add a factory wrapper around your existing interfaces to handle assigning the context at object creation and have that factory return the IDatabase instance. This is basically what you are proposing manually but with an abstraction around it that you could register and inject to keep the code that leverages it from being responsible for the logic:

    public interface ITenantDatabaseFactory {
       public IDatabase GetDatabaseForTenant(int TenantId);
    }
    
    // Add an implementation that manually generates and returns the scoped objects
    
Related