X

Tenant-based dependency injection in multi-tenant ASP.NET Core applications

One need in multitenant applications is injecting dependencies based on tenant configuration. It can be actually more complex as instances may need constructor parameters. Here is my example of dynamic injection of multiple file clients in ASP.NET Core multi-tenant web application.

Source code available! Source code for all my ASP.NET Core multi-tenant application posts is available at public Github repository gpeipman/AspNetCoreMultitenant.

The case of file client

We are living in cloud era. It means that customers of our multitenant application may use different cloud services for their files. Of course, some customers want us to take care of everything. So, how to handle this situation? One customers wants files to be stored on Azure Blob Storage and another wants to go with Google Drive.

We can go with IFileClient generalization I have introduced in my previous posts.

NB! To keep things mininal I’m using dummy IFileClient and implementing classes in this blog post. The main focus here is on tenant based dependency injection.

Dummy file clients

Here are the dummy file clients I’m using for this post.

public interface IFileClient
{
}

public class AzureBlobStorageFileClient : IFileClient
{
    public AzureBlobStorageFileClient(string connectionString)
    {
    }
}

public class GoogleDriveFileClient : IFileClient
{
    public GoogleDriveFileClient(string connectionString)
    {
    }
}

They don’t have any members besides constructor that takes storage connection string as an argument.

Configuring tenants

I will go with JSON-file tenants provider from my blog post Implementing tenant providers on ASP.NET Core. Notice that all tenants have storage type and storage connection string specified in their configuration.

[
  {
    "Id": 1,
    "Host": "sme1:5000",
    "DatabaseType": 1,
    "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=MultitenantWeb_SME;Trusted_Connection=True;MultipleActiveResultSets=true",
    "Name": "Small biz 1",
    "StorageType": "GoogleDrive",
    "StorageConnectionString""<storage connection string 1>"
  },
  {
    "Id": 2,
    "Host": "sme2:5000",
    "DatabaseType": 1,
    "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=MultitenantWeb_SME;Trusted_Connection=True;MultipleActiveResultSets=true",
    "Name": "Small biz 2",
    "StorageType": "AzureBlob",
    "StorageConnectionString""<storage connection string 2>"
  },
  {
    "Id": 3,
    "Host": "bigcorp:5000",
    "DatabaseType": 2,
    "ConnectionString": "Server=localhost;Database=multitenant;Uid=demo;Pwd=demo",
    "Name": "Big corp",
    "StorageType": "AzureBlob",
    "StorageConnectionString": "<storage connection string 3>"
  }
]

Tenant 1 with host name sme1 uses Google Drive for files and tenants 3 with host name bigcorp uses Azure blob storage.

Tenant-based dependency injection

It’s time to head to application Startup class and make dependency injection of IFileClient consider also current tenant. I will use implementation factory function to return correct file client instance from dependency injection.

services.AddScoped<IFileClient>(service =>
{
    var provider = service.GetRequiredService<ITenantProvider>();
    var tenant = provider.GetTenant();

    if(tenant.StorageType == "GoogleDrive")
    {
        return new GoogleDriveFileClient(tenant.ConnectionString);
    }

    if(tenant.StorageType == "AzureBlob")
    {
        return new AzureBlobStorageFileClient(tenant.ConnectionString);
    }

    return null;
});

For unknown and missing file clients my code returns null. It’s not perfect joice but works for this demo.

Trying out file client injection

To see if file clients are injected correctly I will try file client injection out with sme1 and bigcorp tenants.

Both tenants get the correct type of instance as expected.

From here

It’s possible to have more complex cases when dynamic dependency injection is needed in multi-tenant application. There are some interesting works to check out:

Wrapping up

Using my previous works on multi-tenant ASP.NET Core applications and IFileClient generalization we were able to define file client type and connection string in tenants configuration. Using implementation factory method and tenant provider we were able to inject correct instance of file client based on current tenant to ASP.NET Core controller.

Liked this post? Empower your friends by sharing it!

View Comments (6)

  • Wount it be better and more testable to have this logic in a provider? and leave the configure services as logic free as possible?

  • Technically it is possible to put this logic to provider but in this case all devs in projects must know and remember to ask implementations from tenant provider. I don't like this idea because then there are two sources for instances: framework-level dependency injection and tenant provider.

    I wouldn't worry about testing. For unit tests we have to build fakes if needed and we can mock. For integration and acceptance test we can "replace" real startup class with testing one like shown here: https://gunnarpeipman.com/aspnet-core-integration-test-startup/

    Perhaps the most polite option is to use some third-party DI/IoC framework that supports multiple profiles or child containers. In this post I decided to keep things minimal and avoid using these.

  • I would avoid resolving dependencies using this approach (the way you're resolving the ITenantProvider, using the "service" which is a IServiceProvider... becasue the framework actually gives you the root service provider instance to use there, and the root service provider essentially has the lifetime of the web application.. therefore you risk turning scoped dependencies into singletons which last as long as the web app is alive.

    Andrew Lock helped me undersand this, with his article here: https://andrewlock.net/the-dangers-and-gotchas-of-using-scoped-services-when-configuring-options-in-asp-net-core/

    Essentially my advice to never use that root service provider, do not use primitives as dependencies (just wrap them in a class that you can register as a singleton in the IServicesCollection) , and do not rely on configuration values that you need from other instances of your own that you would need to resolve just as you're doing it here... pull it out in a config object with you can populate with IConfiguration.

  • I would be surprised if tenant provider will become singleton as it uses IHttpContextAccessor as dependency. HttpContext is available only with HTTP request. Do you want to say that root service provider can't handle scoped dependencies that have scoped dependencies and therefore tenants can be detected incorrectly? This far I haven't seen such a mess with my solution that Andrew describes.

Related Post