JaviScript
JaviScript

Reputation: 23

How to do DI with DbContext that will change connection string in runtime? (C# Net 6)

in my team we have multiple databases, one for every client that works with us. In our API we only work with one database, so we do dependency injection of the DbContext using the connection string loaded inside appsettings (one instance of this API for every client). Now we have to access different databases with the same structure, and we don't know how to work properly with dependency injection in this case.

The way we have to obtain the connection string for every client is querying a parametry database that has them in a table.

enter image description here

We receive a request that contains that client_id and, based on that id, we obtain the connection string and use it to access the database.

So we must find a way to do DI after we have obtained that connection string that I need in that moment.

Upvotes: 1

Views: 1411

Answers (1)

Kazi Rahiv
Kazi Rahiv

Reputation: 681

Instead of passing connection string like this you can adapt the following multi tenancy pattern.

Define Tenant

public class Tenant
{
    public string Name { get; set; }
    public string TID { get; set; }
    public string ConnectionString { get; set; }
}

Tenant Settings

public class TenantSettings
{
    public Configuration Defaults { get; set; }
    public List<Tenant> Tenants { get; set; }
}

And Configuration

 public class Configuration
{
    public string DBProvider { get; set; }
    public string ConnectionString { get; set; }
}

Your connection string structure will be following like this

"TenantSettings": {
  "Defaults": {
    "DBProvider": "mssql",
    "ConnectionString": "Data Source=(localdb)\\mssqllocaldb;Initial Catalog=sharedTenantDb;Integrated Security=True;MultipleActiveResultSets=True"
  },
  "Tenants": [
    {
      "Name": "alpha",
      "TID": "alpha",
      "ConnectionString": "Data Source=(localdb)\\mssqllocaldb;Initial Catalog=alphaTenantDb;Integrated Security=True;MultipleActiveResultSets=True"
    },
    {
      "Name": "beta",
      "TID": "beta",
      "ConnectionString": "Data Source=(localdb)\\mssqllocaldb;Initial Catalog=betaTenantDb;Integrated Security=True;MultipleActiveResultSets=True"
    }
  ]
}

Our tenant service will be like this

public interface ITenantService
{
    public string GetDatabaseProvider();
    public string GetConnectionString();
    public Tenant GetTenant();
}

Implementation

public class TenantService : ITenantService
{
    private readonly TenantSettings _tenantSettings;
    private HttpContext _httpContext;
    private Tenant _currentTenant;
    public TenantService(IOptions<TenantSettings> tenantSettings, IHttpContextAccessor contextAccessor)
    {
        _tenantSettings = tenantSettings.Value;
        _httpContext = contextAccessor.HttpContext;
        if (_httpContext != null)
        {
            if (_httpContext.Request.Headers.TryGetValue("tenant", out var tenantId))
            {
                SetTenant(tenantId);
            }
            else
            {
                throw new Exception("Invalid Tenant!");
            }
        }
    }
    private void SetTenant(string tenantId)
    {
        _currentTenant = _tenantSettings.Tenants.Where(a => a.TID == tenantId).FirstOrDefault();
        if (_currentTenant == null) throw new Exception("Invalid Tenant!");
        if (string.IsNullOrEmpty(_currentTenant.ConnectionString))
        {
            SetDefaultConnectionStringToCurrentTenant();
        }
    }
    private void SetDefaultConnectionStringToCurrentTenant()
    {
        _currentTenant.ConnectionString = _tenantSettings.Defaults.ConnectionString;
    }
    public string GetConnectionString()
    {
        return _currentTenant?.ConnectionString;
    }
    public string GetDatabaseProvider()
    {
        return _tenantSettings.Defaults?.DBProvider;
    }
    public Tenant GetTenant()
    {
        return _currentTenant;
    }
}

Extend the DBContext

public class ApplicationDbContext : DbContext
{
    public string TenantId { get; set; }

    private readonly ITenantService _tenantService;

    public ApplicationDbContext(DbContextOptions options, ITenantService tenantService) : base(options)
    {
        _tenantService = tenantService;
        TenantId = _tenantService.GetTenant()?.TID;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var tenantConnectionString = _tenantService.GetConnectionString();
        if (!string.IsNullOrEmpty(tenantConnectionString))
        {
            var DBProvider = _tenantService.GetDatabaseProvider();
            if (DBProvider.ToLower() == "mssql")
            {
                optionsBuilder.UseSqlServer(_tenantService.GetConnectionString());
            }
        }
    }
}

There you have it, You can access your current tenant through DI (ITenantService) and have your current DBContext configured also you can do several operation depends on the TenantId .

Upvotes: 1

Related Questions