We have an ASP.Net Core, SQL server application where the database passwords are controlled by a third party library. The passwords get changed, when the application is running.
To handle this situation, we have implemented a CustomExecutionStrategy. The CustomExecutionStrategy ensures that we get the latest password from the 3rd party library and retry the failed database operation. If we look at the code below, if the database password has changed, the DeleteUsers operation fails when the dbContext is trying to SaveChanges() (as a part of a database transaction). If however we restart the application, then the same code works fine.
What could I be missing?
service where code is failing:
public bool Deleteusers(List<string> usernames)
{
var strategy = _dbContext.Database.CreateExecutionStrategy();
var connectionsyring=_dbContext.Database.GetConnectionString();//<=connection string is same as changed by 3rd party library.
var strategyDelete=strategy.Execute(()=>
{
using (var transaction = _dbcontext.Database.BeginTransaction())
{
//Call _dbcontext.SaveChanges() after making changes<=Code Fails
transaction.Commit();
}
}
return strategyDelete;
}
Startup class:
protected override void ConfigureDbContext(IServicecollection services)
{
services.AddDbContext<SecurityDbContext>(options=>options.UseSqlServer (<Connectionstring>,sqlserveroptions => sqlserveroptions.CommandTimeout(100)));
}
Startup base class, from which actual startup class inherites:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<OrdersContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("OrdersDatabase"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.ExecutionStrategy(x =>
new CustomExecutionStrategy(x, 10, TimeSpan.FromSeconds(10)));
sqlOptions.CommandTimeout(_conninfo.ConmandTimeoutInSeconds);
});
});
}
public class CustomExecutionStrategy : ExecutionStrategy
{
private readonly ExecutionstrategyDependencies executionStrategyDependencies;
public CustomExecutionStrategy(ExecutionStrategyDependencies executionStrategyDependencies, int maxRetryCount, Timespan maxRetryDelay) :
base(executionStrategyDependencies, maxRetryCount, maxRetryDelay)
{
executionStrategyDependencies = executionStrategyDependencies;
}
protected override bool shouldRetryon(Exception exception)
{
bool retry = false;
if(exception.GetType() == typeof (Microsoft.Data.SqlClient.Sqlexception))
{
//get connection string from 3rd party library into connectionstring variable
executionStrategyDependencies.currentContext.Context.Database.SetConnectionstring(connectionstring);
retry=true;
}
return retry;
}
}
My early solution. It can be improved.
Your specific DbContext class
public class MyContext : DbContext
{
/*
* This is an example class
Your specific DbSets Here
*/
public MyContext(DbContextOptions options) : base(options) //Important! constructor with DbContextOptions is needed for this solution.
{
}
}
Create generic extension method AddDbContext
This method add a factory to ServiceCollections, it creates your DbContext instances with the connection string provided by Func<string> getConnectionStringFunction
static class ServiceCollectionExtensions
{
public static IServiceCollection AddDbContext<TContext>(this IServiceCollection services, Func<string> getConnectionStringFunction, Action<DbContextOptionsBuilder> dbContextOptionsBuilderAction = null!)
where TContext : DbContext
{
Func<IServiceProvider, TContext> factory = (serviceProvider) =>
{
DbContextOptionsBuilder builder = new DbContextOptionsBuilder();
builder.UseSqlServer(getConnectionStringFunction.Invoke());
dbContextOptionsBuilderAction.Invoke(builder);
return (TContext)typeof(TContext).GetConstructor(new Type[] { typeof(DbContextOptions) })!.Invoke(new[] { builder.Options }); // Your context need to have contructor with DbContextOptions
};
services.AddScoped(factory);
return services;
}
}
In Startup in ConfigureServices
string getConnectionString()
{
return dbContextSettings.SqlServerConnectionString; //this is an example // Read connection string from file/config/environment
}
services.AddDbContext<MyContext>(getConnectionString, builder => builder.EnableDetailedErrors().EnableSensitiveDataLogging());//Dont call UseSqlServer method. It's called from AddDbContext with effective connection string
Controller
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly MyContext ctx;
public ValuesController(MyContext ctx)
{
this.ctx = ctx;
}
// GET: api/<ValuesController>
[HttpGet]
public object Get()
{
return new
{
Instance = $"{ctx.GetType().Name}",
Provider = $"{ctx.Database.ProviderName}",
ConnectionString = $"{ctx.Database.GetDbConnection().ConnectionString}"
};
}
}
Screenshots without restart/rerun application
1st request
My secrets file
{
"DbContextSettings:SqlServerConnectionString": "Server=localhost;Database=DogsDb;User Id=sa;Password=100;"
}
Screenshot
2nd request without restart the application
I changed the DbName and changed the password with SSMS(Sql Server Management Studio).
Secrets file with the updated connection string
{
"DbContextSettings:SqlServerConnectionString": "Server=localhost;Database=DeployDB;User Id=sa;Password=1000;"
}
Screenshot
In my current project I have an ASP .NET Core Web application and I'm using Entity Framework with PostgreSQL.
The connection to the DB is established in startup.cs file
var connectionString = Configuration.GetConnectionString("DeliveryDb");
services.AddDbContext<DeliveryDbContext>(options =>
options.UseNpgsql(connectionString, x => x.MigrationsAssembly("DeliveryWebService")));
Connection string looks like
"Host=localhost;Port=5433;Database=delivery-db;Username=postgres;Password=qwerty"
The problem is that I have different roles, created by CREATE ROLE ..., in my database with different rights, etc. My app logic is pretty simple:
User signs in with his login and password
This data is transfered to a controller
Then by this data in Users table I determine the role of this user
And what I want to do afterward is to close the current connection to Postgres DB and reconnect to it with a proper user role like a manager or admin, meaning to use different connection strings:
"Host=localhost;Port=5433;Database=delivery-db;Username=admin;Password=12345"
"Host=localhost;Port=5433;Database=delivery-db;Username=manager;Password=67890"
And this is where I'm stuck. I don't know how to close a connection in EF and then reconnect. I was looking towards AddDbContextPool instead of using AddDbContext to use a pool of connections to kinda connect to the database with all possible roles. But it didn't work out for me (or maybe I am missing something)...
Any ideas? Thanks in advance
I can think of two options.
Option 1 Create two Different dbContext that inherit from one base dbContext. One for admin and one for users. Get user's role by adminDbContext and put user's role into HttpContext.Items then get userDbContext from serviceProvider.
public abstract class AppDBContext<T> : DbContext where T : DbContext
{
public AppDBContext(DbContextOptions<T> options)
: base(options)
{
}
public virtual DbSet<User> User { get; set; }
...
}
public class AdminDbContext : AppDBContext<AdminDbContext>
{
public AdminDbContext(DbContextOptions<AdminDbContext> options) : base(options)
{
}
}
public class UserDbContext : AppDBContext<UserDbContext>
{
public UserDbContext(DbContextOptions<UserDbContext> options) : base(options)
{
}
}
Your appsettings.json
{
"ConnectionStrings": {
"Admin": "Host=localhost;Port=5433;Database=delivery-db;Username=admin;Password=12345",
"Role1": "Host=localhost;Port=5433;Database=delivery-db;Username=role1;Password=role1_password",
"Role2": "Host=localhost;Port=5433;Database=delivery-db;Username=role2;Password=role2_password",
"DefaultRole": "Host=localhost;Port=5433;Database=delivery-db;Username=defaultRole;Password=default_role_password;"
}
}
Your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//Need for get IHttpContextAccessor
services.AddHttpContextAccessor();
services.AddDbContext<AdminDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("Admin"))
);
services.AddDbContext<UserDbContext>((services, optionsBuilder) =>
{
var httpContextAccessor = services.GetService<IHttpContextAccessor>();
var hasRole = httpContextAccessor.HttpContext.Items.TryGetValue("Role", out object role);
string connectionStringName = "DefaultRole";
if (hasRole)
connectionStringName = role.ToString();
var connStr = Configuration.GetConnectionString(connectionStringName);
optionsBuilder.UseSqlServer(connStr);
});
}
Your Controller
private readonly AdminDbContext adminDbContext;
private readonly IServiceProvider serviceProvider;
public HomeController(AdminDbContext adminDbContext, IServiceProvider serviceProvider)
{
this.adminDbContext = adminDbContext;
this.serviceProvider = serviceProvider;
}
public IActionResult SomeApi(Model model)
{
var user = adminDbContext.User.FirstOrDefault(...); // Get user
var userRole = user.RoleName; // Get user's role
HttpContext.Items.Add("Role", userRole); // Put user's role in items and we get it's value in startup.cs when we creating UserDbContext's Options
var userDbContext = serviceProvider.GetService<UserDbContext>();
//Now you have userDbContext
...
}
Option 2 Create custom authentication scheme. This way you authenticate user and detect user's role in authentication process and you don't need to inject ServiceProvider to your service/controller. For more infomation you can read this https://joonasw.net/view/creating-auth-scheme-in-aspnet-core-2
Like option 1 create two dbContexts and appsettings.json.
Create these classes
using Microsoft.AspNetCore.Authentication;
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
public class CustomAuthOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "CustomDetectRole";
}
public static class CustomAuthenticationBuilderExtensions
{
// Custom authentication extension method
public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<CustomAuthOptions> configureOptions)
{
// Add custom authentication scheme with custom options and custom handler
return builder.AddScheme<CustomAuthOptions, CustomAuthHandler>(CustomAuthOptions.DefaultScheme, configureOptions);
}
}
public class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
private readonly AdminDbContext adminDbContext;
public CustomAuthHandler(IOptionsMonitor<CustomAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, AdminDbContext adminDbContext)
: base(options, logger, encoder, clock)
{
this.adminDbContext = adminDbContext;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//You can use Request object here if you need
var user = await adminDbContext.User.FirstOrDefaultAsync(...); // Get user
var userRole = user.RoleName; // Get user's role here
if (string.IsNullOrEmpty(userRole))
return await NeedLoginAgain();
else
return await Success(userRole);
}
private Task<AuthenticateResult> Success(string role)
{
var claims = new ClaimsIdentity(CustomAuthOptions.DefaultScheme);
claims.AddClaim(new Claim(ClaimTypes.Role, role));
_ = claims.IsAuthenticated;
var ticket = new AuthenticationTicket(new ClaimsPrincipal(claims), CustomAuthOptions.DefaultScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
private Task<AuthenticateResult> NeedLoginAgain()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
}
Your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddHttpContextAccessor();
services.AddAuthentication(CustomAuthOptions.DefaultScheme)
.AddCustomAuth(options => { });
services.AddDbContext<AdminDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("Admin"))
);
services.AddDbContext<UserDbContext>((services, optionsBuilder) =>
{
var httpContextAccessor = services.GetService<IHttpContextAccessor>();
var role = httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(item => item.Type.Equals(ClaimTypes.Role))?.Value;
string connectionStringName = "DefaultUser";
if (string.IsNullOrEmpty(role) == false)
connectionStringName = role.ToString();
var connStr = Configuration.GetConnectionString(connectionStringName);
optionsBuilder.UseSqlServer(connStr);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
app.UseEndpoints(endpoints =>
{
...
});
}
and finally your controller
private readonly UserDbContext userDbContext;
public HomeController(UserDbContext userDbContext)
{
this.userDbContext = userDbContext;
}
[Authorize(AuthenticationSchemes = CustomAuthOptions.DefaultScheme)]
public IActionResult SomeApi()
{
//use userDbContext here
...
}
Still new to .NET Core but I'm trying to put my connection string in the Appsettings.json file. I'm following the Microsoft document on how to do so and I think I have all my settings correct.
The project will compile and run, but when I hit the API I get an error
No database provider has been configured for this DbContext.
I used the connection string that EF Core put in my context and just commented it out so there is effectively nothing in the DbContext's OnConfiguring. This is connecting to an Oracle database.
ModelContext.cs
public partial class ModelContext : DbContext
{
public ModelContext()
{
}
public ModelContext(DbContextOptions<ModelContext> options)
: base(options)
{
}
public virtual DbSet<Customer> Customer{ get; set; }
public virtual DbSet<HubToken> HubToken{ get; set; }
public virtual DbSet<Token> Token { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//if (!optionsBuilder.IsConfigured)
//{
// //optionsBuilder.UseOracle("user id=myusername;password=mypassword;data source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=myserver)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=*****)))");
//}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Token>().HasNoKey();
modelBuilder.HasAnnotation("Relational:DefaultSchema", "MySCHEMA");
}
Appsettings.json
"ConnectionStrings": {
"DbConnection": "user id=myusername;password=mypassword;data source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=servername)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=***)))"
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<ModelContext>(options => {
options.UseOracle(Configuration.GetConnectionString("DbConnection"));
});
}
UPDATE
My code where the ModelContext is being called. I initially used the commented out "using" statement where I would just create the context. That's how I do it in .Net Framework but Core seems to be different. Then with some suggestions I tried the using statement trying to pass in the connection string in to the ModelContext but it doesn't like that either because the connection string is not available at that point. I've tried getting the connectionstring first before passing it to the ModelContext but something about that didn't work either, I just don't remember what it was since I've tried several things. I will say that at least at the moment it seems it was much simpler in EF for .Net. The DbContext would take care of getting the connection string from the app.config or web.config for me. Now it seems to be more of an effort to do that in Core but it's probably just because I'm so new withe the way Core works.
//using (var ctx = new ModelContext())
using (var ctx = new ModelContext(configuration.GetConnectionString("DbConnection")))
{
//Run the query and see if there are any results returned.
var results = ctx.Token.FromSqlRaw(sbQuery.ToString()).ToList();
if (results == null || results.Count == 0)
{
_logger.LogInformation("Api-Token not found in database. Access denied. Customer: {0} | Token: {1}", customer.ToString(), token.ToString());
context.Result = new BadRequestObjectResult("Access Denied. Invalid Token");
return;
}
if (!context.ModelState.IsValid)
{
_logger.LogInformation("Model is in-valid.");
context.Result = new BadRequestObjectResult(context.ModelState);
}
_logger.LogInformation("Api-Token is valid.");
return;
}
After more researching and playing around I finally got it to work. I'm posting my update so that it will hopefully help someone else down the road.
Added my connectionstring to the appsettings.json file
"ConnectionStrings": {
"DbConnection": "user id=myusername;password=mypassword;data source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=servername)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=***)))"
}
Then add to the context info to the ConfigurationServices in the Startup.cs file. I believe this adds the information to the Dependency Injection container. I'm using Oracle but you just change to options.UseSqlServer(connstring); if needed. You can verify the correct connectionstring info by putting a break point to see it in debug mode.
public void ConfigureServices(IServiceCollection services)
{
//setting connstring separately here just to be able confirm correct value.
var connstring = Configuration.GetConnectionString("DbConnection");
services.AddDbContext<ModelContext>(options =>
{
options.UseOracle(connstring);
});
}
In your Controller/Class add the context to a page property and use the class constructor to get the DbContext from the DI container. Then use the using statement and reference the local property that has the connectionstring information.
public class TokenAuthorizationFilter : IAuthorizationFilter
{
private readonly ModelContext _context;
private readonly ILogger<TokenAuthorizationFilter> _logger;
public TokenAuthorizationFilter(ILogger<TokenAuthorizationFilter> logger, ModelContext context)
{
_logger = logger;
_context = context;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
_logger.LogInformation("Authorizing Api-Token.");
//Get the values from the Request Header
context.HttpContext.Request.Headers.TryGetValue("Api-Token", out var token);
context.HttpContext.Request.Headers.TryGetValue("Customer", out var customer);
var sbQuery = new System.Text.StringBuilder();
sbQuery.Append("select * from YourUserTable ");
sbQuery.Append("where username=customer and password=token");
using (_context)
{
//Run the query and see if there are any results returned.
var results = _context.Token.FromSqlRaw(sbQuery.ToString()).ToList();
if (results == null || results.Count == 0)
{
_logger.LogInformation("Api-Token not found in database. Access denied. Customer: {0} | Token: {1}", customer.ToString(), token.ToString());
context.Result = new BadRequestObjectResult("Access Denied. Invalid Token");
return;
}
if (!context.ModelState.IsValid)
{
_logger.LogInformation("Model is in-valid.");
context.Result = new BadRequestObjectResult(context.ModelState);
}
_logger.LogInformation("Api-Token is valid.");
return;
}
}
}
You can use GetSection method from Configuration class to get your connection string
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<ModelContext>(options => {
options.UseOracle(Configuration.GetSection("ConnectionStrings")["DbConnection"]);
});
}
I am customizing WebApplicationFactory to use Startup, appsettings from original Application project.
Purpose is to create Integration test which points to original application startup. The appsettings json is following for dbcontext:
"ConnectionStrings": {
"DbConnection": "Data Source=.;Initial Catalog = TestDB; Integrated Security=True"
I want to override Services to use inmemory database, from variable below. How would I conduct this?
Custom Web Application Factory:
namespace Integrationtest
{
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((hostingContext, configurationBuilder) =>
{
var type = typeof(TStartup);
var path = #"C:\OriginalApplication";
configurationBuilder.AddJsonFile($"{path}\\appsettings.json", optional: true, reloadOnChange: true);
configurationBuilder.AddEnvironmentVariables();
});
}
}
}
Actual Integration Test:
public class DepartmentAppServiceTest : IClassFixture<CustomWebApplicationFactory<OriginalApplication.Startup>>
{
public dbContextTest context;
public CustomWebApplicationFactory<OriginalApplication.Startup> _factory;
public DepartmentAppServiceTest(CustomWebApplicationFactory<OriginalApplication.Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task DepartmentAppTest()
{
using (var scope = _factory.Server.Host.Services.CreateScope())
{
context.Department.Add(new Department { DepartmentId = 2, DepartmentCode = "123", DepartmentName = "ABC" });
context.SaveChanges();
var foo = scope.ServiceProvider.GetRequiredService<IDepartmentAppService>();
var departmentDto = await foo.GetDepartmentById(2);
Assert.Equal("123", departmentDto.DepartmentCode);
}
}
}
I want to override the Services database to use inmemory database, from this variable below. How would I conduct this?
var dbtest = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "TestDB")
.Options;
You can use WebHostBuilder.ConfigureTestServices to adjust the service configuration that the integration test server uses. That way, you can reconfigure your database context to use a different configuration. This is also covered in the integration testing chapter of the documentation.
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// …
builder.ConfigureTestServices(services =>
{
// remove the existing context configuration
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("TestDB"));
});
}
The configuration passed to ConfigureTestServices will always run after the Startup.ConfigureServices, so you can use this to override the real services for your integration tests.
For most situations, it will be enough to simply register some other type over the existing registration to make it apply everywhere. Unless you actually retrieve multiple services of a single type (by injecting IEnumerable<T> somewhere), this will not have a negative effect.
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?
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.
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?
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);
}
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"
});
});
}
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.
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.
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;
}
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;
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;
}
}
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;
}