1

I am implementing a webapi with ASP.NET Core 3.

On the back-end we have a number of company databases with the exact same schema/tables etc.

I am trying to implement a company database context provider that will let me resolve the correct company database context at runtime.

In my startup.cs I have the below code which should give you an idea what I'm trying to do:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CompanyDbContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("CompanyDbConnection")));

    services.AddScoped<ICompanyRepository, CompanyRepository>();

    services.AddDbContext<System.Func<CompanyType, ICompanyDbContext>>(dbContextOptionsBuilder => key =>
    {
        switch (key)
        {
            case CompanyType.HKG:
                return dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString("HkgCompanyDbConnection"));
            case CompanyType.SHG:
                return dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString("ShgCompanyDbConnection"));
            default:
                throw new KeyNotFoundException();
        }
    });

    //...

Instead of the generic CompanyContext which will only have 1 connection I want to inject something like a service provider but for the dbcontexts into the CompanyRepository constructor, which will then let me resolve different company dbcontexts in different cases in the repository functions (by passing arguments).

The idea is the last part of the code should return the correct context based on the key provided. But obviously the last part is not working. If it is not obvious the repository class will hold all the functions returning data from the database.

How would one go about this ?

Nkosi
  • 235,767
  • 35
  • 427
  • 472
RoleyBaxter
  • 223
  • 3
  • 14
  • Do you have a single context pointing to a single customer DB or multiple contexts pointing to multiple customer DB's per request (scoped)? – SilentTremor Oct 04 '19 at 11:40
  • per request I will have multiple contexts they are injected separately, but there will only ever be 1 company-context pointing to 1 company-database, the rest are separate. So I want to avoid injecting all 20 or so "sibling" contexts. I want to inject 1 common context for these which can resolve to any one of the 20 or so company databases at runtime, depending on the nature of the request. Yes "Scoped" would be ideal for this context – RoleyBaxter Oct 04 '19 at 11:45
  • So it's one per request, you cant have two open connections to different customer DB's, or I'm getting this wrong – SilentTremor Oct 04 '19 at 11:57
  • Yes, you are correct. Only 1 per request. – RoleyBaxter Oct 04 '19 at 11:58

2 Answers2

0

Following the comments this is how you can do it, by checking what is the company Id or name on the request, I have a doubt this is what you need in the end.

Oh, yes I forgot, this code was tested on .net core 2.2, but it should make a difference.

    services.AddScoped<ICompanyType, CompanyType>();
    services.AddEntityFrameworkSqlServer();
    services.AddDbContext<CompanyDbContext>((serviceProvider, options) =>
        {
            var companyCode = serviceProvider.GetRequiredService<ICompanyType>().Get();
            string connectionString = string.Empty;
            switch (companyCode)
            {
                case CompanyType.HKG:
                    connectionString = Configuration.GetConnectionString("HkgCompanyDbConnection");
                    break;
                case CompanyType.SHG:
                    connectionString = Configuration.GetConnectionString("ShgCompanyDbConnection");
                    break;
                default:
                    throw new InvalidOperationException();
            }
            options
                .UseSqlServer(connectionString)
                .UseInternalServiceProvider(serviceProvider);
        });

And some dummy company type implementation:

public class CompanyType: ICompanyType
{
    public const string HKG = "HKG";
    public const string SHG = "SHG";
    public string Get()
    {
        Random rand = new Random((int) DateTime.UtcNow.Ticks & 0x0000FFFF);
        var flipCoin = rand.Next(0, 1);
        return flipCoin == 0 ? HKG : SHG;
    }
}

public interface ICompanyType
{
    // dummy code, to be replaced with what is needed
    string Get();
}
SilentTremor
  • 4,747
  • 2
  • 21
  • 34
  • Thanks, I have not tested this yet, as the other solution was more close to what I already had and did not require me to make a custom class for CompanyType (it's just an enum). Maybe for someone else this is better if you have more complex needs with the switching key class. – RoleyBaxter Oct 04 '19 at 13:57
0

I did something fairly similar (but not quite) recently and came across this excellent Answer which I think does exactly what you're after.

The basic steps:

Shared Delegate

public delegate IService ServiceResolver(CompanyType key);

Add Instances to be Resolved

//Note: Default Service Lifetime for AddDbContext is Scoped - perfect!
services.AddDbContext<HkgCompanyContext>(options...);
services.AddDbContext<ShgCompanyContext>(options...);

Resolver Service

services.AddScoped<ServiceResolver>(serviceProvider => key =>
{
   switch (key)
   {
      case CompanyType.HKG:
         return serviceProvider.GetService(HkgCompanyContext);
      case CompanyType.SHG:
         return serviceProvider.GetService(ShgCompanyContext);
      default:
         throw new KeyNotFoundException();
   }
}
meshtron
  • 1,110
  • 9
  • 19
  • This ought to work for me, have not tested it yet, but seems to be a clean solution. Only concern is that it requires adding the empty sub-classes for each diff company-context, when only the connection string is the difference. But that seems like a pretty small complaint. – RoleyBaxter Oct 04 '19 at 13:51