53

I want to change sql connection string in controller, not in ApplicationDbContext. I'm using Asp.Net Core and Entity Framework Core.

For example:

public class MyController : Controller {
    private readonly ApplicationDbContext _dbContext
    public MyController(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    private void ChangeConnectionString()
    {
    // So, what should be here?
    } }

How can I do this?

Yurii N.
  • 5,455
  • 12
  • 42
  • 66

10 Answers10

64

This is enough if you want to choose a connection string per http request, based on the active http request's parameters.

    using Microsoft.AspNetCore.Http;

    //..

    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

    services.AddDbContext<ERPContext>((serviceProvider, options) =>
        {
            var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;
            var httpRequest = httpContext.Request;
            var connection = GetConnection(httpRequest);
            options.UseSqlServer(connection);
        });

Update

A year or so later, my solution looks like bits and pieces from other answers here, so allow me to wrap it up for you.

You could add a singleton of the HttpContextAccessor on your startup file:

services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDbContext<ERPContext>();

This will resolve the injection on your context constructor:

public class ERPContext : DbContext
{
    private readonly HttpContext _httpContext;

    public ERPContext(DbContextOptions<ERPContext> options, IHttpContextAccessor httpContextAccessor = null)
        : base(options)
    {
        _httpContext = httpContextAccessor?.HttpContext;
    }

    //..

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            var clientClaim = _httpContext?.User.Claims.Where(c => c.Type == ClaimTypes.GroupSid).Select(c => c.Value).SingleOrDefault();
            if (clientClaim == null) clientClaim = "DEBUG"; // Let's say there is no http context, like when you update-database from PMC
            optionsBuilder.UseSqlServer(RetrieveYourBeautifulClientConnection(clientClaim));
        }
    }

    //..
}

And this will give you a clean way to access and extract a claim and decide your connection.

As @JamesWilkins stated on the comments, OnConfiguring() will be called for each instance of the context that is created.

Notice the optional accessor and the !optionsBuilder.IsConfigured. You will need them to ease your tests where you would be overriding your context configuration.

ginalx
  • 1,905
  • 1
  • 15
  • 19
  • 3
    Interesting. You should be more clear about why this works; from the docs: `optionsAction: An optional action to configure the DbContextOptions for the context. This provides an alternative to performing configuration of the context by overriding the OnConfiguring(DbContextOptionsBuilder) method in your derived context.` ... `[OnConfiguring()] is called for each instance of the context that is created` – James Wilkins Jan 31 '19 at 15:02
  • 1
    Love this solution and thank you James for providing some insight into the magic behind it. This was exactly the solution we were looking for! – xinunix Mar 23 '19 at 02:10
  • Do you know if this is still needed with EF core 5? https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-5-0-preview-1/#comments – TWilly May 02 '20 at 13:57
  • How to implement this in `Autofac` registration? – Balagurunathan Marimuthu May 08 '20 at 07:30
  • 1
    It works like a charm with EF Core 3.1.5. Thank you! – ThanhLD Jul 21 '20 at 04:01
  • 1
    So the solution uses the `clientClaim` to initialize the database via the options object it is getting through the method `RetrieveYourBeautifulClientConnection(clientClaim)`. What happens if you're using this inside an WebAPI where different users can be on different databases? Can we still use TryAddSingleton there or does it need to be scoped or transient? – Matt Aug 14 '20 at 11:24
  • @Matt I don't write c# any more but if I recall correctly, it is worth investigating if scoped or transient would be better. And I'm actually pretty sure they would. Nice catch. – ginalx Aug 14 '20 at 11:55
  • Thanks for your answer. – Matt Aug 14 '20 at 14:38
  • @MariINova every http request could theoretically contain a username and password in order to authenticate and authorize the request. Instead of that, you could use a token containing these claims of who the request is performed by. You need to setup an authentication process for that. Create token on login, use token on every subsequent request, decode token into a readable claim collection. You need to read more on the subject. [example read](https://devblogs.microsoft.com/aspnet/bearer-token-authentication-in-asp-net-core/) – ginalx Dec 27 '20 at 16:34
24

We have a case similar to you. What we've done is use the implementationfactory overload of the IServiceCollection in the ConfigureServices method of the Startup class, like so:

//First register a custom made db context provider
services.AddTransient<ApplicationDbContextFactory>();
//Then use implementation factory to get the one you need
services.AddTransient(provider => provider.GetService<ApplicationDbContextFactory>().CreateApplicationDbContext());

It is very difficult for me right now to implement CreateApplicationDbContext for you, because it totally depends on what you want exactly. But once you've figured that part out how you want to do it exactly, the basics of the method should look like this anyway:

public ApplicationDbContext CreateApplicationDbContext(){
  //TODO Something clever to create correct ApplicationDbContext with ConnectionString you need.
} 

Once this is implemented you can inject the correct ApplicationDbContext in your controller like you did in the constructor:

public MyController(ApplicationDbContext dbContext)
{
    _dbContext = dbContext;
}

Or an action method in the controller:

public IActionResult([FromServices] ApplicationDbContext dbContext){
}

However you implement the details, the trick is that the implementation factory will build your ApplicationDbContext everytime you inject it.

Tell me if you need more help implementing this solution.

Update #1 Yuriy N. asked what's the difference between AddTransient and AddDbContext, which is a valid question... And it isn't. Let me explain.

This is not relevant for the original question.

BUT... Having said that, implementing your own 'implementation factory' (which is the most important thing to note about my answer) can in this case with entity framework be a bit more tricky than what we needed.

However, with questions like these we can nowadays luckily look at the sourcecode in GitHub, so I looked up what AddDbContext does exactly. And well... That is not really difficult. These 'add' (and 'use') extension methods are nothing more than convenience methods, remember that. So you need to add all the services that AddDbContext does, plus the options. Maybe you can even reuse AddDbContext extension method, just add your own overload with an implementation factory.

So, to come back to your question. AddDbContext does some EF specific stuff. As you can see they are going to allow you to pass a lifetime in a later release (transient, singleton). AddTransient is Asp.Net Core which allows you to add any service you need. And you need an implementation factory.

Does this make it more clear?

Danny van der Kraan
  • 5,344
  • 6
  • 31
  • 41
  • Why you choose to add `DbContext` with `AddTransient`? What's the difference between this method of adding and `services.AddEntityFramework().AddSqlServer().AddDbContext(options => options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]))`? And with yours method, we lost migrations from console, don't we? – Yurii N. Apr 25 '16 at 12:49
  • I've updated my answer in response to you. Hope it helps. :) – Danny van der Kraan Apr 25 '16 at 14:01
  • It really helpful, thanks! But still unclear, what should we do with migrations? `ApplicationDbContext` not `DbContext`, so migrations from console don't work. – Yurii N. Apr 25 '16 at 14:10
  • What do you mean by that? ApplicationDbContext should/could still inherit from DbContext, right? Maybe try to implement the solution. See how far you can get. Then post a new specific question on SO and we'll help you further. :) – Danny van der Kraan Apr 26 '16 at 09:49
  • yes, you right, let's try to implement the solution. – Yurii N. Apr 26 '16 at 10:00
  • 1
    For me, it is not so clear how you implemented this. I have the issue that DbContext - if it gets injected by DI, it has a fixed database connection (which cannot be changed because you can only pass the options via constructor and the datasource is read-only afterwards, it has only a getter). What I need is to instanciate DbContext for each user, because the users can be on different servers/databases. I don't see how this implementation would solve it. – Matt Aug 14 '20 at 11:28
16

I was able to change the connection string for each request by moving the connection string logic into the OnConfiguring method of the DbContext.

In Startup.cs#ConfigureServices method: services.AddDbContext<MyDbContext>();

In MyDbContext.cs, I added the services I needed injected to the constructor.

    private IConfigurationRoot _config;
    private HttpContext _httpContext;

    public MyDbContext(DbContextOptions options, IConfigurationRoot config, IHttpContextAccessor httpContextAccessor) 
          : base(options)
    {
        _config = config;
        _httpContext = httpContextAccessor.HttpContext;
    }

Then override OnConfiguring:

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connString = BuildConnectionString(); // Your connection string logic here

        optionsBuilder.UseSqlServer(connString);
    }
jhr
  • 653
  • 5
  • 12
  • How did you actually do the injection of config and httpContextAccessor? Maybe you used something other than AddDbContext? – starmandeluxe May 31 '17 at 00:35
  • You need to add their injecting in `ConfigureServices()`, something like this: `services.AddTransient()`. Important to inject them before doing `addDbContext()`. – Yurii N. Jun 01 '17 at 08:58
  • @YuriyN. when I add services.AddDbContext(), it is calling only the default constructor of MyDbContext class which is public MyDbContext (){ }. before calling this, i hv already injected httpcontext. – user1447718 Jun 26 '17 at 05:26
  • @user1447718 If, I'm not mistaken, you shouldn't have default constructor, only constructor with injected services, which you need. – Yurii N. Jun 26 '17 at 08:57
  • 3
    how do you actually pass your connection string into constructor, saying " your connection string logic here " is not good enough, I don't want my logic to be there, I want my connection string to be determined before context is being build – Aistis Taraskevicius Jul 07 '17 at 15:12
  • 3
    A perfect solution for adding database-per-tenant multitenancy to an ASP.NET Core application! – MartinH Oct 28 '17 at 20:05
  • In order to get this working on .NET Core 2.1 I had to add the following to Startup.cs: ```public Startup(IConfiguration configuration, IHostingEnvironment env) { Configuration = configuration; HostingEnvironment = env; }``` and then I was able to set a cookie on login and then read this in the OnConfiguring section of the DbContext like so: ```var connString = _config.GetConnectionString(dbConnection); optionsBuilder.UseSqlServer(connString);``` – Robin Wilson Nov 25 '18 at 17:43
16

The answers of @ginalx and @jcmordan fit my use case perfectly. The thing I like about these answers is that I can do it all in Startup.cs and keep all other classes clean of construction code. I want to supply an optional querystring parameter to a Web Api request and have this substituted into the base connection string which creates the DbContext. I keep the base string in the appsettings.json, and format it based on the passed in parameter or a default if none supplied, i.e:

"IbmDb2Formatted": "DATABASE={0};SERVER=servername;UID=userId;PWD=password"

Final ConfigureServices method for me looks like (obvs. I am connecting to DB2 not SQL, but that's incidental):

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();

        services.AddDbContext<Db2Context>(((serviceProvider, options) =>
        {
            var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;
            var httpRequest = httpContext.Request;

            // Get the 'database' querystring parameter from the request (if supplied - default is empty).
           // TODO: Swap this out for an enum.
            var databaseQuerystringParameter = httpRequest.Query["database"].ToString();

            // Get the base, formatted connection string with the 'DATABASE' paramter missing.
            var db2ConnectionString = Configuration.GetConnectionString("IbmDb2Formatted");

            if (!databaseQuerystringParameter.IsNullOrEmpty())
            {
                // We have a 'database' param, stick it in.
                db2ConnectionString = string.Format(db2ConnectionString, databaseQuerystringParameter);
            }
            else
            {
                // We havent been given a 'database' param, use the default.
                var db2DefaultDatabaseValue = Configuration.GetConnectionString("IbmDb2DefaultDatabaseValue");
                db2ConnectionString = string.Format(db2ConnectionString, db2DefaultDatabaseValue);
            }

            // Build the EF DbContext using the built conn string.
            options.UseDb2(db2ConnectionString, p => p.SetServerInfo(IBMDBServerType.OS390));
        }));

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new Info
            {
                Title = "DB2 API",
                Version = "v1"
            });
        });
    }
Ciaran
  • 543
  • 5
  • 14
13

Although late, but the simplest trick in EF Core is using nuget Microsoft.EntityFrameworkCore.Relational:

_dbContext.Database.GetDbConnection().ConnectionString = "NEW_CONN_STRING";

This is useful when a connection string is not present in your application config/settings for any reason or you want to deal with multiple databases with same structure using one instance of DbContext (again, for any reason).

Being Permanently or Temporarily depends on type the injection life-cycle you choose for DbContext. It will be permanent if you inject it as Singleton service, which is not recommended.

Efe
  • 800
  • 10
  • 32
  • 2
    Thanks, this works for me to change to different databases with same structure on the same server & credentials. To keep the password, the connectionstring must contain: Persist Security Info=True; Example: `public void SetDatabase(string newDatabase) { var con = _context.Database.GetDbConnection(); var csb = new SqlConnectionStringBuilder(con.ConnectionString) { InitialCatalog = newDatabase }; con.ConnectionString = csb.ConnectionString;}` – Pete Nov 12 '20 at 16:22
  • it was what I needed, but I don't have the method "GetDbConnection()" in "_context.Database" – MariINova Dec 01 '20 at 12:26
  • @MariINova please install the nuget package Microsoft.EntityFrameworkCore.Relational. link: https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Relational/ – Efe Dec 01 '20 at 21:43
  • 1
    Nice solution to set Connection string in Base Controller to use specific database depending on user that makes call. – Ivica Buljević Jul 29 '21 at 23:51
  • Could you explain please how to pass the connection string to the controllers constructor? – Thomas Arnold Feb 14 '22 at 11:18
  • @ThomasArnold Actually, the best practice is to store connection strings in appsettings.json and in order to access them, simply inject IConfiguration anywhere you want and access connection strings. https://stackoverflow.com/a/45109353/7565464 I do not recommend changing conn string in controllers, but check the link. – Efe Feb 14 '22 at 12:29
11

All other answers did not worked for me. so I would like to share my approach for the people who work to change DB connection string at runtime.

My application was built with asp.net core 2.2 with Entity Framework and MySql.

StartUp.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddDbContext<MyDbContext>();

    ...

MyDbContext Class

public partial class MyDbContext : DbContext
{
    public MyDbContext()
    {
    }

    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (DbManager.DbName != null && !optionsBuilder.IsConfigured)
        {
            var dbName = DbManager.DbName;
            var dbConnectionString = DbManager.GetDbConnectionString(dbName);
            optionsBuilder.UseMySql(dbConnectionString);
        }
    }

    ...

Json - File that has a Connection Info

[
  {
    "name": "DB1",
    "dbconnection": "server=localhost;port=3306;user=username;password=password;database=dbname1"
  },
  {
    "name": "DB2",
    "dbconnection": "server=localhost;port=3306;user=username;password=password;database=dbname2"
  }
]

DbConnection Class

using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;


public class DbConnection
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("dbconnection")]
    public string Dbconnection { get; set; }

    public static List<DbConnection> FromJson(string json) => JsonConvert.DeserializeObject<List<DbConnection>>(json, Converter.Settings);
}

    internal static class Converter
    {
        public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
        {
            MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
            DateParseHandling = DateParseHandling.None,
            Converters =
            {
                new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
            },
        };
    }
}

DbConnectionManager Class

public static class DbConnectionManager
{
    public static List<DbConnection> GetAllConnections()
    {
        List<DbConnection> result;
        using (StreamReader r = new StreamReader("myjsonfile.json"))
        {
            string json = r.ReadToEnd();
            result = DbConnection.FromJson(json);
        }
        return result;
    }

    public static string GetConnectionString(string dbName)
    {
        return GetAllConnections().FirstOrDefault(c => c.Name == dbName)?.Dbconnection;
    }
}

DbManager Class

public static class DbManager
{
    public static string DbName;

    public static string GetDbConnectionString(string dbName)
    {
        return DbConnectionManager.GetConnectionString(dbName);
    }
}

Then, you would need some controller that set dbName up.

Controller Class

[Route("dbselect/{dbName}")]
public IActionResult DbSelect(string dbName)
{
    // Set DbName for DbManager.
    DbManager.DbName = dbName;

    dynamic myDynamic = new System.Dynamic.ExpandoObject();
    myDynamic.DbName = dbName;
    var json = JsonConvert.SerializeObject(myDynamic);
    return Content(json, "application/json");
}

You might have to do some trick something here and there. but you will get the Idea. At the beginning of the app, It doesn't have connection detail. so you have to set it up explicitly using Controller. Hope this will help someone.

Jin Lim
  • 1,759
  • 20
  • 24
  • Yet another great approach, but why don't simply inject `IOptions` and configure your connections string with all the great power of ASP.NET Core built-in configuration? – Yurii N. Jul 17 '19 at 17:01
  • Because, If so, It's harder to add more connection when it needed. If you store in Json or other DB. You can just simply add a connection. hope this making you sense. – Jin Lim Jul 17 '19 at 17:26
  • 2
    [You can read ASP.NET Core's configuration even from INI config files!](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.2#file-configuration-provider) – Yurii N. Jul 18 '19 at 18:21
  • Good document. but In order to get value. you need to create `IConfiguration config` in somewhere at the constructor. Then you won't be able to make DbManager class and method as `static`. For Handling dbConnection, my approach still better option I reckon. – Jin Lim Jul 19 '19 at 10:55
  • where is 'Converter.Settings' coming from? It just appears. Including the using statements would help. – Mark Worrall Jul 29 '19 at 11:17
  • @Mark Worrall Updated answer. – Jin Lim Jul 29 '19 at 15:12
  • 1
    Working great with SQL Server. – hubert17 Oct 11 '19 at 16:21
  • So, if I get the idea right, you have pre-configured the available connections in the .json config file and you pick one of them by name. – Matt Aug 14 '20 at 11:09
4

That work for me:

public void ConfigureServices(IServiceCollection services)
{
    // .....
    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddTransient<School360DbContext>(provider =>
    {
        return ResolveDbContext(provider, hostingEnv);
    });
    // ..
}

private MyDbContext ResolveDbContext(IServiceProvider provider, IHostingEnvironment hostingEnv)
{
    string connectionString = Configuration.GetConnectionString("DefaultConnection");

    string SOME_DB_IDENTIFYER = httpContextAccessor.HttpContext.User.Claims
        .Where(c => c.Type == "[SOME_DB_IDENTIFYER]").Select(c => c.Value).FirstOrDefault();
    if (!string.IsNullOrWhiteSpace(SOME_DB_IDENTIFYER))
    {
        connectionString = connectionString.Replace("[DB_NAME]", $"{SOME_DB_IDENTIFYER}Db");
    }

    var dbContext = new DefaultDbContextFactory().CreateDbContext(connectionString);

    // ....
    return dbContext;
}
jcmordan
  • 1,038
  • 13
  • 20
2

I went for this solution:

Instead of

services.AddScoped<IMyDbContext, MyDbContext>();

I went for

services.AddTransient<IMyDbContext, MyDbContext>(resolver =>
{
    var context= resolver.GetService<MyDbContext>();
    var config = resolver.GetService<IConfiguration>();
    var connectionString = config.GetConnectionString("MyDb");
    context.GetDbConnection().ConnectionString = connectionString;
    return context;
});

Overwrite setting at runtime:

Configuration["ConnectionStrings:MyDb"] = newConnectionString;
MovGP0
  • 7,267
  • 3
  • 49
  • 42
2

I created a .net6 console app and loop 1 to 10 for inserting to test1 database and test2 database: Program.cs :

Console.WriteLine("Hello, World!");

for (int i = 1; i <= 10; i++)
{

    if (i % 2 == 0)
    {
        var _context = new AppDbContext("Data Source=.\\SQLEXPRESS;Initial Catalog=test2;Integrated Security=True"); // test2
        _context.Tbls.Add(new Tbl { Title = i.ToString() });
        _context.SaveChanges();
    }
    else
    {
        var _context = new AppDbContext("Data Source=.\\SQLEXPRESS;Initial Catalog=test1;Integrated Security=True"); // test1
        _context.Tbls.Add(new Tbl { Title = i.ToString() });
        _context.SaveChanges();
    }
}

AppDbContext.cs :

public partial class AppDbContext : DbContext
    {
        public AppDbContext(string connectionString) : base(GetOptions(connectionString))
        {
        }

        public virtual DbSet<Tbl> Tbls { get; set; }

        private static DbContextOptions GetOptions(string connectionString)
        {
            return SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder(), connectionString).Options;
        }
    }
M Komaei
  • 7,006
  • 2
  • 28
  • 34
1

Startup.cs for static connection

services.AddScoped<MyContext>(_ => new MyContext(Configuration.GetConnectionString("myDB")));

Repository.cs for dynamic connection

using (var _context = new MyContext(@"server=....){
context.Table1....
}

Table1MyContext.cs

public MyContext(string connectionString) : base(GetOptions(connectionString))
{
}

private static DbContextOptions GetOptions(string connectionString)
{
    return SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder(), connectionString).Options;
}
Lakmal
  • 779
  • 1
  • 8
  • 16
  • One question - where do I get `SqlServerDbContextOptionsExtensions` from? – Matt Aug 14 '20 at 12:50
  • Found it [here](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.sqlserverdbcontextoptionsextensions.usesqlserver?view=efcore-3.1#Microsoft_EntityFrameworkCore_SqlServerDbContextOptionsExtensions_UseSqlServer_Microsoft_EntityFrameworkCore_DbContextOptionsBuilder_System_Data_Common_DbConnection_System_Action_Microsoft_EntityFrameworkCore_Infrastructure_SqlServerDbContextOptionsBuilder__) – Matt Aug 14 '20 at 12:56