I configure my database context by allowing changing of the connectionString based on the users cookie:
services.AddDbContext<HotdogContext>((serviceProvider, dbContextBuilder) =>
{
string connectionString = "DatabaseUS"; // Default connection string
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var dbNameCookie = httpContextAccessor.HttpContext.Request.Cookies.ContainsKey("country");
if (dbNameCookie) // debug
{
Console.Write("Country cookie exists!");
}
var country = httpContextAccessor.HttpContext.Request.Cookies["country"]?.ToString() ?? "null";
if (country.Equals("null"))
{
Console.WriteLine("Country cookie is null");
}
else if (country.Equals("USA"))
{
Console.WriteLine("Set USA connection string");
connectionString = "DatabaseUS";
}
else if (country.Equals("AUS"))
{
connectionString = "DatabaseAUS";
Console.WriteLine("Set Australia connection string");
}
else if (country.Equals("S.A."))
{
//connectionString = "DatabaseSA";
Console.WriteLine("Set South America connection string");
}
dbContextBuilder.UseSqlServer(Configuration.GetConnectionString(connectionString));
});
This clutters the startup.cs if I add additional databaseContexts to the program. Is there a way I can hide this logic from startup.cs and use extension methods?
services.AddDbContext<HotdogContext>((serviceProvider, dbContextBuilder) =>
{
string connectionString = "DatabaseUS"; // Default connection string
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var country = dbContextBuilder.GetRequestedCountry(httpContextAccessor); // Extension method
if(country !=null) connectionString = country;
dbContextBuilder.UseSqlServer(Configuration.GetConnectionString(connectionString));
});
One way is to define a service interface, then implement with your logic:
public interface IConnectionStringNameService
{
string GetName();
}
public class ConnectionStringNameFromCookieService
: IConnectionStringNameService
{
private readonly HttpContext _httpContext;
public ConnectionStringNameFromCookieService(
HttpContextAccessor httpContextAccessor )
{
_httpContext = httpContextAccessor.HttpContext;
}
public string GetName()
{
var country = _httpContext.Request
.Cookies[ "country" ]
?? "default value"; // or throw exception
return country switch =>
{
"USA" => "DatabaseUS",
"AUS" => "DatabaseAUS",
"S.A." => "DatabaseSA",
_ => "Default name or throw",
}
}
}
Then you register the service implementation with the container:
services.AddScoped<IConnectionStringNameService, ConnectionStringNameFromCookieService>();
Then you can resolve this service from the service provider when you need need the connection string name:
var connStrNameSvc = serviceProvider.GetRequiredService<IConnectionStringNameService>.();
var connStrName = connStrNameSvc.GetName();
In place (in the startup file), the above would abstract to:
services.AddDbContext<HotdogContext>(
(serviceProvider, dbContextBuilder) =>
{
var connStrName = serviceProvider
.GetRequiredService<IConnectionStringNameService>()
.GetName();
dbContextBuilder.UseSqlServer(
Configuration.GetConnectionString( connStrName ) );
});
But you can then use an extension method to abstract this call to a single, simple line:
public class BlahBlahExtensionMethods
{
// come up with a better name
public IServiceCollection AddDbContextUsingCookieForConnStrName<TDbContext>(
this IServiceCollection services )
where TDbContext : DbContext
{
return services.AddDbContext<TDbContext>(
( serviceProvider, dbContextBuilder ) =>
{
var connStrName = serviceProvider
.GetRequiredService<IConnectionStringNameService>()
.GetName();
dbContextBuilder.UseSqlServer(
Configuration.GetConnectionString( connStrName ) );
});
}
}
Then in startup you'd only need:
services.AddDbContextUsingCookieForConnStrName<HotdogContext>();
Or you can inject the name provider into HotdogContext and use it in an OnConfiguring( DbContextOptionsBuilder ) method override.
Related
Using this answer to mock the IConfiguration methods from an ASP.NET Core app.
I need to mock an IConfigurationSection to return a string array.
My configuration class looks like this:
public class LoggingConfiguration
{
public string ApplicationName { get; set; }
public string[] Loggers { get; set; }
}
appsettings.json
{
"Logging": {
"LoggingConfiguration": {
"ApplicationName": "Some app",
"Loggers": [ "DiskLogger", "MemoryLogger"],
"DiskLogger": {
"SomeSettingOne" : "setting",
"SomeSettingTwo" : "setting",
},
"MemoryLogger": {
"AnotherSetting": "...",
}
}
}
In setting up the mocks - I have two problems.
I can't figure out how to mock an IConfigurationSection that would return string[] (loggers)
I am getting an exception when I try to setup the GetChildren() method on the LoggingSectionMock
public void Setup()
{
var applicationNameConfigurationSectionMock = new Mock<IConfigurationSection>();
applicationNameConfigurationSectionMock.Setup(m => m.Value).Returns(TestingApplicationName);
var loggerNamesConfigurationSectionMock = new Mock<IConfigurationSection>();
loggerNamesConfigurationSectionMock.Setup(m => m.GetChildren()).Returns(GetLoggerNamesSection);
//Throwing Method Not Found exception
LoggingSectionMock.Setup(m => m.GetChildren()).Returns(new List<IConfigurationSection>
{applicationNameConfigurationSectionMock.Object, loggerNamesConfigurationSectionMock.Object});
ConfigurationMock.Setup(m => m.GetSection($"{Logging}:{LoggingConfiguration}"))
.Returns(() => LoggingSectionMock.Object);
}
private IEnumerable<IConfigurationSection> GetLoggerNamesSection()
{
var loggerNamesConfigurationSections = new List<IConfigurationSection>();
LoggerNames.ToList().ForEach(loggerName =>
{
var configSectionMock = new Mock<IConfigurationSection>();
configSectionMock.Setup(m => m.Value).Returns(loggerName);
loggerNamesConfigurationSections.Add(configSectionMock.Object);
});
return loggerNamesConfigurationSections;
}
As an alternative you can take advantage of ConfigurationBuilder's AddInMemoryCollection:
Reference Memory configuration provider
Setup
IConfiguration configRoot = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "ApplicationName", "App" },
{ "Loggers:0", "1" },
{ "Loggers:1", "2" },
{ "Loggers:2", "3" }
})
.Build();
Usage
LoggingConfiguration config = configRoot.Get<LoggingConfiguration>();
I'm using the Mediatr library to register and call my RequestHandlers.
Everything went fine until I started reading more about integrated tests.
PLEASE READ AFTER EDIT
I can't call my class which inherits from the RequesHandler.
My class looks like this:
public class MyRequestHandler : RequestHandler<MyRequest, MyResponse>
{
....
}
I'm not using the Meditr async and I'm using .net framework 4.7 instead of asp.net core, so, everything looks like returns me answers for asp.net core.
When I construct MyTestClass, to construct the RequestHandler I have to create a ServiceFactory and maybe this is the problem because I don't know how.
public MyClassTest()
{
ServiceFactory sv = null;
_mediator = new Mediator(sv);
}
EDIT
Providing more info
I have this Handler in my Application Layer
public class LogInUserByFormHandler : RequestHandler<LogInUserByFormRequest, LogInUserByFormResponse>
{
private readonly IValidator<LogInUserByFormRequest> _validator;
public LogInUserByFormHandler(IValidator<LogInUserByFormRequest> validator)
{
_validator = validator;
}
protected override LogInUserByFormResponse Handle(LogInUserByFormRequest request)
{
_validator.ValidateAndThrow(request);
var userInfo = GetUserInfo(request);
ValidateLogInUserByFormRules(userInfo);
var userLoginInfo = GetValidUserLoginInfo(request);
ValidateUserLoginInfoByFormRules(userLoginInfo);
var sessionKey = CreateUserSessionKey(userInfo);
var response = new LogInUserByFormResponse
{
UserName = request.UserName,
SessionKey = sessionKey,
UserId = userInfo.id_usuario
};
return response;
}
//A LOT OF CODE HERE, methods and etc
}
As it's possible to see, it implements the Mediatr.
On my Web Project on Presentation Layer, I used AutoFac to Inject the Handlers, so, any Request I do is always handled by the right method.
All I have to do is call, like this:
var logInByFormRequest = new LogInUserByFormRequest
{
UserName = viewModel.UserName,
Password = viewModel.Password
};
var response = _mediator.Send(logInByFormRequest).Result;
This works like a charm. The problem now is on the Test project. It references the Application as the Presentation Project does.
I don't know how to make the mediator.send find the right method.
EDIT²
Here comes my test code
[TestClass]
public class LogInUserByFormTest
{
private LogInUserByFormRequest CreateRequest(string userName, string password)
{
LogInUserByFormRequest request = new LogInUserByFormRequest
{
UserName = userName,
Password = password
};
return request;
}
[TestMethod]
[Description("")]
public void UserName_ShouldHave_Max_30Characters_Exception()
{
try
{
var request = CreateRequest("UserNameIsGreaterThanAllowed", "password");
var mediator = new Mock<IMediator>();
var response = mediator.Object.Send(request).Result;
}
catch (System.Exception ex)
{
throw;
}
}
}
The result (response) is always null and the mediator doesn't call the right handler.
EDIT3
Here is how I register the handlers and validators.
I use autofac. This class here is called on the global.asax
public class AutofacConfig
{
public static void ConfigureContainer()
{
var builder = new ContainerBuilder();
builder.RegisterControllers(Assembly.GetExecutingAssembly()).InstancePerRequest();
builder.RegisterType<Mediator>().As<IMediator>().InstancePerLifetimeScope();
builder.RegisterType<AutofacValidatorFactory>().As<IValidatorFactory>().SingleInstance();
builder.RegisterType<FluentValidationModelValidatorProvider>().As<ModelValidatorProvider>();
builder.RegisterType<RegistryManagerService>().As<IRegistryManagerService>().SingleInstance().WithParameter("appName", ConfigurationManager.AppSettings["APPNAME"]);
builder.Register<ServiceFactory>(context =>
{
var c = context.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
builder.RegisterAssemblyTypes(Assembly.Load("Docspider.Application"))
.Where(x => x.Name.EndsWith("Handler"))
.AsImplementedInterfaces();
builder.RegisterAssemblyTypes(Assembly.Load("Docspider.Application"))
.Where(x => x.Name.EndsWith("Validator"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
}
public class AutofacValidatorFactory : ValidatorFactoryBase
{
private readonly IComponentContext _context;
public AutofacValidatorFactory(IComponentContext context)
{
_context = context;
}
public override IValidator CreateInstance(Type validatorType)
{
if (_context.TryResolve(validatorType, out object instance))
{
var validator = instance as IValidator;
return validator;
}
return null;
}
}
For such an integration test you would need to configure the necessary dependencies. Since you have indicated that Autofac is being used then configure a container just as you would have in production. Use the container to get the mediator and perform the desired test.
For example.
[TestClass]
public class LogInUserByForm_IntegrartionTest {
private LogInUserByFormRequest CreateRequest(string userName, string password) {
LogInUserByFormRequest request = new LogInUserByFormRequest {
UserName = userName,
Password = password
};
return request;
}
IMediator BuildMediator() {
//AutoFac
var builder = new ContainerBuilder();
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly).AsImplementedInterfaces();
var mediatrOpenTypes = new[] {
typeof(IRequestHandler<,>)
};
foreach (var mediatrOpenType in mediatrOpenTypes) {
builder
.RegisterAssemblyTypes(typeof(LogInUserByFormRequest).GetTypeInfo().Assembly)
.AsClosedTypesOf(mediatrOpenType)
.AsImplementedInterfaces();
}
builder.Register<ServiceFactory>(ctx => {
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
//...all other needed dependencies.
//...
var container = builder.Build();
var mediator = container.Resolve<IMediator>();
return mediator;
}
[TestMethod]
[Description("")]
public async Task UserName_ShouldHave_Max_30Characters_Exception() {
try
{
//Arrange
var request = CreateRequest("UserNameIsGreaterThanAllowed", "password");
var mediator = BuildMediator();
//Act
var response = await mediator.Send(request);
//Assert
//...assert the expected values of response.
}
catch (System.Exception ex)
{
throw;
}
}
}
The above was modeled after the examples provided by MediatR.Examples.Autofac
I'm looking to have all OkObjectResult responses coming out of my api get run through a custom JSON resolver that I have. The resolver relies on some request-specific data - namely, the user's roles. It's effectively like the Authorize attribute on a controller, but for data transfer objects passed from the API to the UI.
I can add the resolver in Configure Services via AddJsonOptions, but it doesn't have access to that user info there.
How can I pass values that are based on the request to this resolver? Am I looking at some sort of custom middleware, or something else?
As a sample, if I have an object with some custom attribute decorators, like so:
public class TestObject
{
public String Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public String Field2 => "ViewRequiresAdmin";
}
And call my custom serializer with different roles, like so:
var test = new TestObject();
var userRoles = GetRoles(); // "User" for the sake of this example
var outputJson = JsonConvert.SerializeObject(test,
new JsonSerializerSettings {
ContractResolver = new MyCustomResolver(userRoles)
});
Then the output JSON will skip anything the user can't access, like so:
{
"Field1":"NoRestrictions",
// Note the absence of Field2, since it has [RequireRoleView("Admin")]
}
Suppose you have an custom RequireRoleViewAttribute:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
public string Role;
public RequireRoleViewAttribute(string role){
this.Role = role;
}
}
How can I pass values that are based on the request to this resolver?
You can have a IServiceProvider injected in your custom resolver :
public class RoleBasedContractResolver : DefaultContractResolver
{
public IServiceProvider ServiceProvider { get; }
public RoleBasedContractResolver( IServiceProvider sp)
{
this.ServiceProvider = sp;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you're using the Identity, you can get the userManager :
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
// ...
}
}
thus we can get the HttpContext and User as we like. If you're using the Identity, you can also get the UserManager service and roles.
and now we can follow #dbc's advice to control the ShouldSerialize:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
var context = contextAccessor.HttpContext;
var user = context.User;
// if you use the Identitiy, you can get the usermanager
//UserManager<IdentityUser>
var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
JsonProperty property = base.CreateProperty(member, memberSerialization);
// get the attributes
var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
// if no [RequireResoveView] decorated, always serialize it
if(attrs.Count()==0) {
property.ShouldDeserialize = instance => true;
return property;
}
// custom your logic to dertermine wether should serialize the property
// I just use check if it can statisify any the condition :
var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
property.ShouldSerialize = instance => {
var resource = new { /* any you need */ };
return attrs.Any(attr => {
var rolename = attr.Role;
return roles.Any(r => r == rolename ) ;
}) ? true : false;
};
return property;
}
The function GetIdentityUserRolesAsync here is helper method to retrieve roles using the current HttpContext and the UserManger service :
private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
var rolesCached= context.Items["__userRoles__"];
if( rolesCached != null){
return (IList<string>) rolesCached;
}
var identityUser = await userManager.GetUserAsync(context.User);
var roles = await userManager.GetRolesAsync(identityUser);
context.Items["__userRoles__"] = roles;
return roles;
}
How to inject the IServiceProvider in details :
The trick is all about how to configure the default MvcJwtOptions with an IServiceProvider.
Don't configure the JsonOptions by :
services.AddMvc().
.AddJsonOptions(o =>{
// o.
});
as it doesn't allow us add a IServiceProvider parameter.
We can custom a subclass of MvcJsonOptions:
// in .NET 3.1 and above, change this from MvcJsonOptions to MvcNewtonsoftJsonOptions
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}
and register the services by :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// don't forget to add the IHttpContextAccessor
// in .NET 3.1 and above, change this from MvcJsonOptions to MvcNewtonsoftJsonOptions
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
Test Case :
Let's say you have a custom POCO :
public class TestObject
{
public string Field1 => "NoRestrictions";
[RequireRoleView("Admin")]
public string Field2 => "ViewRequiresAdmin";
[RequireRoleView("HR"),RequireRoleView("OP")]
public string Field3 => "ViewRequiresHROrOP";
[RequireRoleView("IT"), RequireRoleView("HR")]
public string Field4 => "ViewRequiresITOrHR";
[RequireRoleView("IT"), RequireRoleView("OP")]
public string Field5 => "ViewRequiresITOrOP";
}
And the Current User has roles : Admin and HR:
The result will be :
{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}
A screenshot of testing with an action method :
Itminus's answer covers everything that's needed, but for anyone interested, I've extended it a little for easy reuse.
First, in a class library
My RequireRoleViewAttribute, which allows multiple roles (OR, not AND):
[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
public List<String> AllowedRoles { get; set; }
public RequireRoleViewAttribute(params String[] AllowedRoles) =>
this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}
My resolver is almost identical to Itminus's, but CreateProperty is adjusted to
IEnumerable<String> userRoles = this.GetIdentityUserRoles();
property.ShouldSerialize = instance =>
{
// Check if every attribute instance has at least one role listed in the user's roles.
return attrs.All(attr =>
userRoles.Any(ur =>
attr.AllowedRoles.Any(ar =>
String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
);
};
And GetIdentityUserRoles doesn't use UserManager
private IEnumerable<String> GetIdentityUserRoles()
{
IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext context = contextAccessor.HttpContext;
ClaimsPrincipal user = context.User;
Object rolesCached = context.Items["__userRoles__"];
if (rolesCached != null)
{
return (List<String>)rolesCached;
}
var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
context.Items["__userRoles__"] = roles;
return roles;
}
And I have an extensions class which contains:
public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
return services;
}
Then in my API
I reference that class library. In Startup.cs -> ConfigureServices, I call:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddRoleBasedContractResolver();
...
}
And my DTOs are tagged with the attribute:
public class Diagnostics
{
public String VersionNumber { get; set; }
[RequireRoleView("admin")]
public Boolean ViewIfAdmin => true;
[RequireRoleView("hr")]
public Boolean ViewIfHr => true;
[RequireRoleView("hr", "admin")]
public Boolean ViewIfHrOrAdmin => true;
}
And the return value as an admin is:
{
"VersionNumber": "Debug",
"ViewIfAdmin": true,
"ViewIfHrOrAdmin": true
}
Overview:
I have two projects, one ASP.NetCore Web API and one EF 6.1 Class Library. The class library contains manager classes that interact with my database and return objects.
I followed this tutorial for dependency injection, but it does not address the scenario in my project where EF is in a separate class library and interaction takes place via manager classes:
https://learn.microsoft.com/en-us/aspnet/core/data/entity-framework-6
I got things working in the following way (but I am not happy because I don't want to overload the connection string):
public class AccountManager
{
private static string ConnectionString = LibDataEntities.ConnectionString;
public static AccountDC AuthenticateAccount(string EmailAddress, string Password)
{
LibData.Model.AccountDC account = null;
using (var db = new LibDataEntities(ConnectionString))
{
var qry = (from accounts in db.Accounts
where accounts.IsAdmin == true
&& accounts.Active == true
&& accounts.Email == EmailAddress
&& accounts.Password == Password
select new LibData.Model.AccountDC
{
Active = accounts.Active,
Email = accounts.Email,
FirstName = accounts.FirstName,
Id = accounts.Id,
IsAdmin = accounts.IsAdmin,
IsSuperAdmin = accounts.IsSuperAdmin,
LastName = accounts.LastName,
LibraryId = accounts.LibraryId,
Password = accounts.Password
}).FirstOrDefault();
if (qry != null)
{
account = new LibData.Model.AccountDC();
account = qry;
}
}
return account;
}
}
public partial class LibDataEntities
{
public static string ConnectionString;
public LibDataEntities(string connectionString) : base(connectionString)
{
}
}
public Startup(IHostingEnvironment env)
{
// Set connection string in Manager Project
LibData.Manager.Entities.LibDataEntities.ConnectionString = Configuration.GetConnectionString("LibDataEntities");
}
Is it possible to interact with the class library managers WITHOUT having to overload the connection string (such as the example below)?
using (var db = new LibDataEntities())
FWIW: the reason I do not want to overload the connection string is because this class library is used by a non-asp.netcore application where the Web.Config already contains the connecting string.
Here is some new code I am trying without success (as indicated in one of my replies below:
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession();
services.AddAuthorization();
services.AddMvc(options =>
{
options.Filters.Add(new ExceptionFilter());
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddMvcGrid();
services.AddScoped(_ => new AccountManager());
services.AddScoped<LibDataEntities>(_ => new LibDataEntities(Configuration.GetConnectionString("LibDataEntities")));
}
private readonly AccountManager _am;
public HomeController(AccountManager am)
{
_am = am;
}
From the controller:
LibData.Model.AccountDC account = _am.AuthenticateAccount(lvm.Email, lvm.Password);
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;
}