Need a way to get the current request URL to configure the database context in multi-tenant application

Viewed 196

I am migrating a web app from asp.net mvc to .net core (.net 5), and this has got me stuck. The site is configured in IIS to accept request from multiple URLs like site1.example.com and site2.example.com. Each site has its own database, accessed through entity framework core.

In the old .net framework, I was able to use one of the events in the global.asax.cs to parse the incoming request URL and lookup the correct tenant database from a configuration file. I'm trying to set up something similar in asp.net core mvc.

Here's the relevant part of my ConfigureServices method in the startup.cs

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpContextAccessor();
    services.AddSingleton<ITenantIdentifier, UrlTenantIdentifier>();
    services.AddDbContext<myDbContext>((serviceProvider, dbContextBuilder) =>
    {
        var tenantIdentifier = serviceProvider.GetRequiredService<ITenantIdentifier>();
        var connectionString = Configuration.GetConnectionString(tenantIdentifier.GetCurrentTenantId() + "myDataModel");
        dbContextBuilder.UseSqlServer(connectionString);
    }, ServiceLifetime.Scoped);

    //other services configured below...
}

Then the tenant identifier looks like this:

public interface ITenantIdentifier
{
    string GetCurrentTenantId();
}

public class UrlTenantIdentifier : ITenantIdentifier
{
    readonly IHttpContextAccessor _httpContextAccessor;
    readonly ILogger<UrlTenantIdentifier> _logger;

    public UrlTenantIdentifier(IHttpContextAccessor httpContextAccessor, ILogger<UrlTenantIdentifier> logger)
    {
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
    }

    public string GetCurrentTenantId()
    {
        //_httpContextAccessor is null here

        //logic below for parsing URL and finding if we're site1 or site2
    }
}

Is there a correct way of doing this now that I'm not aware of? How can I set up the entity framework database context for dependency injection when I don't know the connection string key until runtime? Am I going to be stuck configuring separate sites and virtual directories in IIS?

1 Answers

Refactor the DbContext to override the OnConfiguring member. Inject configuration and context accessor and perform configuration there.

public class myDbContext : DbContext {
    private readonly ITenantIdentifier tenantIdentifier;
    private readonly IConfiguration configuration;

    public myDbContext(IConfiguration configuration, ITenantIdentifier tenantIdentifier) {
        this.configuration = configuration;
        this.tenantIdentifier = tenantIdentifier;
    }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
        var connectionString = configuration
            .GetConnectionString(tenantIdentifier.GetCurrentTenantId() + "myDataModel");
        optionsBuilder.UseSqlServer(connectionString);
    }
}

Trying to access the request context at the time the DbContext is being created/initialized is too early in the request flow to get access to the desired information. It needs to happen after the context has already been initialized and injected.

public void ConfigureServices(IServiceCollection services)  
    services.AddHttpContextAccessor();
    services.AddSingleton<ITenantIdentifier, UrlTenantIdentifier>();
    services.AddDbContext<myDbContext>(); //Simplified since configuration is internal

    //other services configured below...
}

Reference DbContext Lifetime, Configuration, and Initialization

Related