1

I have an API developed in .NET Core with EF Core. I have to serve multiple clients with different data(but the same schema). This is a school application, where every school want to keep their data separately due to competition etc. So we have a database for each school. Now my challenge is, based on some parameters, I want to change the connection string of my dbContext object.

for e.g., if I call api/students/1 it should get all the students from school 1 and so on. I am not sure whether there is a better method to do it in the configure services itself. But I should be able to pass SchoolId from my client application

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<SchoolDataContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("APIConnectionString")));
    services.AddScoped<IUnitOfWorkLearn, UnitOfWorkLearn>();
}

11 May 2021


namespace LearnNew
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //Comenting to implement Mr Brownes Solution
            //services.AddDbContext<SchoolDataContext>(options =>
            //   options.UseSqlServer(
            //       Configuration.GetConnectionString("APIConnectionString")));



            services.AddScoped<IUnitOfWorkLearn, UnitOfWorkLearn>();


            services.AddControllers();


            services.AddHttpContextAccessor();

            services.AddDbContext<SchoolDataContext>((sp, options) =>
            {
                var requestContext = sp.GetRequiredService<HttpContext>();
                var constr = GetConnectionStringFromRequestContext(requestContext);
                options.UseSqlServer(constr, o => o.UseRelationalNulls());

            });

            ConfigureSharedKernelServices(services);

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "LearnNew", Version = "v1" });
            });
        }

        private string GetConnectionStringFromRequestContext(HttpContext requestContext)
        {
            //Trying to implement Mr Brownes Solution
            var host = requestContext.Request.Host;
            // Since I don't know how to get the connection string, I want to  
            //debug the host variable and see the possible way to get details of 
            //the host. Below line is temporary until the right method is identified
            return Configuration.GetConnectionString("APIConnectionString");
        }

        private void ConfigureSharedKernelServices(IServiceCollection services)
        {
            ServiceProvider serviceProvider = services.BuildServiceProvider();
            SchoolDataContext appDbContext = serviceProvider.GetService<SchoolDataContext>();

            services.RegisterSharedKernel(appDbContext);
        }

            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LearnNew v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }



    }
}
Jacob
  • 81
  • 9

1 Answers1

0

You can access the HttpContext when configuring the DbContext like this:

        services.AddControllers();

        services.AddHttpContextAccessor();

        services.AddDbContext<SchoolDataContext>((sp, options) =>
        {
            var requestContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
            var constr = GetConnectionStringFromRequestContext(requestContext);
            options.UseSqlServer(constr, o => o.UseRelationalNulls());

        });

This code:

            var requestContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;                var constr = GetConnectionStringFromRequestContext(requestContext);
            options.UseSqlServer(constr, o => o.UseRelationalNulls());

will run for every request, configuring the connection string based on details from the HttpRequestContext.

If you need to use your DbContext on startup, don't resolve it through DI. Just configure a connection like this:

        var ob  = new DbContextOptionsBuilder<SchoolDataContext>();
        var constr = "...";
        ob.UseSqlServer(constr);
        using (var db = new Db(ob.Options))
        {
            db.Database.EnsureCreated();
        }

But in production you would normally create all your tenant databases ahead-of-time.

David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • Hello David, Thank you for the response. Since I am new to this, if my question is too basic, please bear with me. Assume I have two schools subscribed for this API, the first school will try to access the student's controller from https://schoolapis.azurewebsites.net/api/students/1 and the second school from https://schoolapis.azurewebsites.net/api/students/2. Since the DbContext is in the startup shall I assume, .net core will run the configure services in both scenarios? I don't want to end up in school 1 getting the student list of school 2 due to whatever reasons. – Jacob May 08 '21 at 16:30
  • This also means that I have to pass the school id as a parameter to all controller actions right? Is there any better way? – Jacob May 08 '21 at 16:30
  • Using a URL parameter for the school ID might not be the best idea. Instead you typically use the identity of the user. The authentication data, request headers, and URL are all avaialble through the HttpContext. – David Browne - Microsoft May 08 '21 at 16:34
  • Thank you, David. I will test the same and confirm the answer. Once again Thank you for your time. I started with DbContextFactory, but your solution looks more straight forward. – Jacob May 08 '21 at 17:13
  • Hi David, it gives me the following error, can you please help me. System.InvalidOperationException: 'No service for type 'Microsoft.AspNetCore.Http.HttpContext' has been registered.' I know while you looking at me from your level I am just an absolute beginner, but I am confident with your help I can get it working. below is the complete code. – Jacob May 10 '21 at 18:16
  • public void ConfigureServices(IServiceCollection services) { services.AddScoped(); services.AddControllers(); services.AddHttpContextAccessor(); services.AddDbContext((sp, options) => { var requestContext = sp.GetRequiredService(); var constr = GetConnectionStringFromRequestContext(requestContext); options.UseSqlServer(constr, o => o.UseRelationalNulls()); }); } – Jacob May 10 '21 at 18:20
  • private string GetConnectionStringFromRequestContext(HttpContext requestContext) { var host = requestContext.Request.Host; return Configuration.GetConnectionString("APIConnectionString"); } private void ConfigureSharedKernelServices(IServiceCollection services) { ServiceProvider serviceProvider = services.BuildServiceProvider(); SchoolDataContext appDbContext = serviceProvider.GetService(); services.RegisterSharedKernel(appDbContext); } – Jacob May 10 '21 at 18:21
  • This is a web api in net5.0 net5.0 – Jacob May 10 '21 at 18:25
  • Please post any updated code in your question. – David Browne - Microsoft May 10 '21 at 19:44
  • Hi David, The entire startup class is pasted below 11 May 2021 – Jacob May 11 '21 at 07:15
  • The error is occuring at sp.GetRequiredService() System.InvalidOperationException HResult=0x80131509 Message=No service for type 'Microsoft.AspNetCore.Http.HttpContext' has been registered. Source=Microsoft.Extensions.DependencyInjection.Abstractions StackTrace: at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) at LearnNew.Startup. – Jacob May 11 '21 at 08:18
  • See updated answer. Looks like you have to request IHttpContextAccessor, not HttpContext. – David Browne - Microsoft May 11 '21 at 16:53
  • Hello David, Thank you for your time. Now the error is gone. Can you please help me with the code for GetConnectionStringFromRequestContext. Now I am getting an error in GetConnectionStringFromRequestContext because the requestContext is null. Is it because in startup there is no HttpContext created yet – Jacob May 11 '21 at 17:09
  • Hi David, I may not need a DbContext in the startup. But if I set a debug in var requestContext = ... its hits only once (at startup). Using swagger, I was trying to request for my API but the debug in the startup is not hitting. Does it mean that It will not hit those lines in every request? I feel I know too little to attempt something like this. I don't want to waste your valuable time on this. However, if you feel you can stand my little knowledge, I request you to help me. Otherwise thank you very much for the help. – Jacob May 11 '21 at 17:20
  • Hi David, MY case is like this. This application is for managing schools. Today I have 1 school subscribed to my application. The application uses Blazor for UI and a web API for data services. Now there are more than 20 schools showing interest, I don't want to configure 20 API. Instead want to use one API and get data from different databases. That is why I want to change the connection string dynamically. – Jacob May 11 '21 at 17:31
  • As you suggested, I can look for user credentials that are stored in ASPNet Identity in a separate database that is accessible by another API service. Once the user is Authenticated I can somehow pass the details to this API. I checked your profile, if my method is not good can you suggest something that can solve my problem. – Jacob May 11 '21 at 17:31
  • You've got the right idea. Startup only runs once. After that you inject your DbContext into your controller through constructor injection. – David Browne - Microsoft May 11 '21 at 17:47
  • Can you please give me some sample codes or projects so that I can follow them? – Jacob May 11 '21 at 18:20
  • I pushed the project I was testing with to GitHub: https://github.com/dbrownems/AspNetCoreEfTest – David Browne - Microsoft May 11 '21 at 18:32
  • Hi David, Hope you are good. After spending some time with the application you send, I created a client app and an API for my requirement. I added the database name as a claim. Now the problem is when I try to access the HttpContext in the web API, it's not showing the claim. I have uploaded all three projects in Git https://github.com/jpthomas74/MutipleDbTest.git (the API you sent, the one I created based on your API (LearnNewMultiConnectionAPI) and the Web App (LearnNewAspNetWebAppWithAuth). If you run the web app and click New API you will know what I am referring to. Can you please help – Jacob May 15 '21 at 18:28