I am trying to unit test with the EF Core in-memory database like so:
namespace ContosoTests
{
public class TrendServiceTests
{
private static Microsoft.EntityFrameworkCore.DbContextOptions<TestContext> options = new
Microsoft.EntityFrameworkCore.DbContextOptionsBuilder<TestContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
private static TestContext _context = new TestContext(options);
private readonly TrendService trendService = new TrendService(_context);
private void SeedInMemoryDb()
{
if (!_context.TrendHistories.Any())
{
_context.TrendHistories.Add(new TrendHistory { IsActive = true, Quarter = E_Quarter.Q1,
TrendYear = 2020 });
}
if (!_context.Controls.Any())
{
_context.Controls.Add(new Control { IsActive = true});
_context.Controls.Add(new Control { IsActive = true});
_context.Controls.Add(new Control { IsActive = false});
}
_context.SaveChanges();
}
}
My TestContext inherits from my live context class to avoid the error:
Services for database providers 'Microsoft.EntityFrameworkCore.InMemory', 'Microsoft.EntityFrameworkCore.SqlServer' have been registered in the service provider. Only a single database provider can be registered in a service provider
So my TestContext just looks like:
public class TestContext : ContosoContext
{
public TestContext(DbContextOptions<TestContext> options)
{
}
public TestContext()
{
}
}
When I use the new instance of TestContext (_context) in my test class, all the live data for ContosoContext is there.
I was expecting it to be a new instance of the context class with no data so that I could test my DAL code with controlled data. But this is not the case.
I am fairly new to unit testing so any help is much appreciated.
Edit:
Here are the relevant parts of my contoso context class
public class ContosoContext: IdentityDbContext<ContosoApplicationUser>
{
public ContosoContext(DbContextOptions<ContosoContext> options) : base
(options)
{
}
public ContosoContext()
{
}
//all my DBSets here
protected override void OnConfiguring(DbContextOptionsBuilder
optionsBuilder)
{
optionsBuilder.UseSqlServer("MyConnectionString");
}
}
So the issue is that my onconfiguring method handles the connection string directly??
To fix the error you need to remove optionsBuilder.UseSqlServer("MyConnectionString"); in OnConfiguring method to avoid registering multiple database providers, change it to this:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer("MyConnectionString");
}
Related
I have a project that uses Microsoft.EntityFrameworkCore.Sqlite 6.0.5 on net6.0. I am expecting the Sqlite database file to be unlocked after the DbContext has been disposed.
However, the behaviour I am observing is that the Sqlite database remains locked after the DbContext has been disposed and finalised. There is a project that reproduces the behaviour here.
How can I unlock the database file?
My DbContext looks like this:
public class MyContext : DbContext
{
~MyContext()
{
Console.WriteLine("Finaliser was called.");
}
public override void Dispose()
{
base.Dispose();
Console.WriteLine("Dispose was called.");
}
public static readonly string DbFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "_temp.db");
public DbSet<Foo> Summaries { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Foo>().HasKey(nameof(Foo.Id));
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlite($"Data Source={DbFile}");
}
}
I am using it like this:
public static void AddItem()
{
using var ctx = new MyContext();
ctx.Database.EnsureCreated();
ctx.Summaries.Add(new Foo {Bar = "Foo"});
ctx.SaveChanges();
}
ClearAllPools() or specify no pooling in connection string (Pooling=false)
https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/connection-strings#pooling
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
I'm using EF Core "In Memory" database but having trouble mocking a view.
var options = new DbContextOptionsBuilder<MyContext>()
.UseInMemoryDatabase(databaseName: "TestDatabase")
.Options;
// Insert seed data into the database using one instance of the context
using (var context = new MyContext(options))
{
// insert test values into entities here (omitted)...
// I want some test values for a View
context.MyView.AddRange( viewEntity1, viewEntity, ... etc );
context.SaveChanges();
}
At runtime, when attempting to add the view entities, the following exception is raised:
System.InvalidOperationException: Unable to track an instance of type
'MyView' because it does not have a primary key. Only entity types
with primary keys may be tracked.
The reason I want to mock the View is that I want to test some LINQ queries that join to the View.
Any ideas?
// Entity
public class MyView
{
public string UserId {get;set;}
public int CentreId {get;set;}
}
// Context
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.Entity<MyView>()
.HasNoKey()
.ToView("MyView");
}
No, the in-memory provider doesn't support views. It's a relational operation, same as the FromSql* and ExecuteSql* methods.
AddRange won't work on a view regardless of the provider.
You can test views; there are a couple of options:
Do integration tests with a real database
Use a better provider, possibly SQLite
Use a testing package that allows you to mock the view
I maintain one of said testing packages, EntityFrameworkCore.Testing, which does allow you to easily unit test SUTs that depend on views.
You should not mock anything at the Entity Framework level. Don't mock what you don't own.
In general, you should not be mocking implementation detail, which is what a view is - The consumer of your code should not need to know that it's interacting with a view, but that it's interacting with some data source. Otherwise your tests will end up breaking when you change the implementation detail, which is not what should be happening.
If you want to test how your controllers (or similar) interact with Entity Framework, you should use the Repository pattern and mock the Repository instead. If that's the case, you should not be mocking anything and instead should use a real database to test against.
If you want to test how your repositories interact with the database, you can use an integration test to test this. Repositories should be simple with very little business logic, so there is not much reason to unit test them to begin with.
Yes, you sure can....
Here is what I am using for my setup
Moq (for...mocks)
Lamar (for Dependency Injection)
STANDARD EF CORE VIEW:
Here the code for one of my view-definitions.
// EntityConfiguration
internal class ProjectStatusDetailConfiguration : IEntityTypeConfiguration<ProjectStatusDetail>
{
public void Configure(EntityTypeBuilder<ProjectStatusDetail> builder)
{
// SQL View
builder.ToView("vProjectStatus", "dbo")
.HasKey(e => e.ProjectId);
}
}
// Concrete DbContext
public class ProjectDbContext : DbContext
{
private IConfigurationRoot _settings;
public ProjectDbContext()
{
// NOTE: You must have this constructor for Migrations to work
BuildAppConfiguration();
}
public ProjectDbContext(DbContextOptions<ProjectDbContext> options) : base(options)
{
// NOTE: This constructor is used at the IServiceProvider layer
BuildAppConfiguration();
}
public virtual DbSet<Project> Project { get; set; }
public virtual DbSet<ProjectStatusDetail> vProjectStatusDetail { get; set; }
private void BuildAppConfiguration()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
_settings = builder.Build();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Default Configuration (if none is provided by the app)
if (!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer(_settings.GetConnectionString(JsonSettings.ConnectionStrings.DefaultDb));
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// GLOBAL Configurations
builder.ApplyGlobalEntityConfiguration(new AuditableStandardUserNameGlobalConfiguration());
// ENTITY Configurations
builder.ApplyConfiguration(new ProjectConfiguration());
// SQL VIEW Configurations
builder.ApplyConfiguration(new ProjectStatusDetailConfiguration());
}
}
// Lamar ServiceRegistry (for the business-layer)
public class ContainerRegistry : ServiceRegistry
{
public ContainerRegistry()
{
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
scan.SingleImplementationsOfInterface();
});
// --------
// DATABASE
For<DbContext>().Use<ProjectDbContext>();
For(typeof(IAuditableRepository<>)).Use(typeof(GenericAuditableRepository<>));
ForConcreteType<ProjectUnitOfWork>().Configure.Setter<DbContext>().Is<ProjectDbContext>();
// Policies
Policies.Add<GenericAuditableRepositoryConfiguredInstancePolicy>(); //<-- Sets CTOR at Runtime
Policies.Add<UnitOfWorkConfiguredInstancePolicy>(); //<-- Sets CTOR at Runtime
}
}
// SINGLE POINT OF ENTRY: Consumes & "serves-up" your App's Registry
public static class IoC
{
public static ServiceRegistry Build()
{
var registry = new ServiceRegistry();
// Registration
registry.IncludeRegistry<ContainerRegistry>();
return new ServiceRegistry(registry);
}
}
UNIT TESTING SETUP:
Here the code for my unit testing setup. Obviously, I am still working-on the DbSetProxy's query...but here it is anyway.
// Lamar ServiceRegistry (for the unit test)
internal class ContainerRegistry : ServiceRegistry
{
public ContainerRegistry()
{
// This is where you will SHIM all your Stubs/Fakes
For<IWindowsIdentityHelper>().Use<UnitTests.WindowsIdentityHelperStub>(); //<-- Stub
}
}
// SINGLE POINT OF ENTRY: Consume all Registry's (for the unit testing-layer)
public static class IoC
{
public static ServiceRegistry Build()
{
var registry = new ServiceRegistry();
// Notice you can bring-in many ServiceRegistry's
// Just be sure to apply your "stubbing" registry last
registry.IncludeRegistry<Bushido.WorkflowComponent.DependencyResolution.ContainerRegistry>();
registry.IncludeRegistry<Bushido.Project.Business.DependencyResolution.ContainerRegistry>();
registry.IncludeRegistry<Bushido.WorkflowComponent.UnitTests.DependencyResolution.ContainerRegistry>();
return new ServiceRegistry(registry);
}
}
// This base class class does the heavy-lifting
public class UnitTestBase
{
protected IContainer Container { get; private set; }
protected Builder Builder { get; set; } = new Builder();
[TestInitialize()]
public virtual void Initialize()
{
IHostBuilder hostBuilder = CreateHostBuilder(null);
IHost host = hostBuilder.Build();
IServiceScope serviceScope = host.Services.CreateScope();
IServiceProvider serviceProvider = serviceScope.ServiceProvider;
Container = serviceProvider.GetRequiredService<IContainer>();
}
[TestCleanup()]
public virtual void Cleanup()
{
Container.Dispose();
Container = null;
}
private IHostBuilder CreateHostBuilder(string[] args)
{
var builder = new HostBuilder()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
})
// You must apply the Lamar Factory
.UseServiceProviderFactory<ServiceRegistry>(new LamarServiceProviderFactory())
.ConfigureServices((hostContext, services) =>
{
var databaseName = Guid.NewGuid().ToString();
// Add Lamar & Apply Scoping here (not in your ServiceRegistry's)
services.AddLamar(IoC.Build());
services.AddScoped<ProjectUnitOfWork>();
services.AddScoped<WorkflowComponentUnitOfWork>();
services.AddScoped<ProjectDbContext>();
services.AddScoped<WorkflowComponentDbContext>();
services.AddDbContext<ProjectDbContext>((provider, options) => options.UseInMemoryDatabase(databaseName), ServiceLifetime.Singleton);
services.AddDbContext<WorkflowComponentDbContext>((provider, options) => options.UseInMemoryDatabase(databaseName), ServiceLifetime.Singleton);
});
return builder;
}
}
// This base class contains the common setup for your VIEWS DbSetProxy
public class DataScenarioBase
{
protected static DbSet<TEntity> CreateDbSet<TEntity>(IQueryable<TEntity> collection) where TEntity : class
{
var stubDbSet = new Mock<DbSet<TEntity>>();
stubDbSet.As<IQueryable<TEntity>>().Setup(m => m.Provider).Returns(collection.Provider);
stubDbSet.As<IQueryable<TEntity>>().Setup(m => m.Expression).Returns(collection.Expression);
stubDbSet.As<IQueryable<TEntity>>().Setup(m => m.ElementType).Returns(collection.ElementType);
stubDbSet.As<IQueryable<TEntity>>().Setup(m => m.GetEnumerator()).Returns(collection.GetEnumerator());
return stubDbSet.Object;
}
}
// This is a sample DataScenario
public class ProjectDefaultDataScenario : DataScenarioBase
{
public void Load(IContainer container)
{
var unitOfWork = container.GetInstance<ProjectUnitOfWork>();
// DATA
LoadDefaultData(unitOfWork) //<-- you can pre-load default data into the UnitOfWork here too
// VIEWS
var dbContext = ((ProjectDbContext)unitOfWork.DbContext);
dbContext.vProjectStatusDetail = CreateDbSet<ProjectStatusDetail>(ProjectStatusDetailQuery(unitOfWork));
// Inject
container.GetNestedContainer().Inject(unitOfWork); //<-- This (along with scoping) ensures the right instance is always passed back
}
private IQueryable<ProjectStatusDetail> ProjectStatusDetailQuery(ProjectUnitOfWork unitOfWork)
{
var query = (from project in unitOfWork.Project
select new ProjectStatusDetail
{
ProjectId = project.Id,
ProjectName = project.ProjectName,
ObjectStateName = "Testing 123",
ObjectStateDateTime = DateTime.Now,
});
return query;
}
}
// Here is a sample Unit Test
[TestMethod]
public void ProjectProvider_WorkflowProvider_ObjectState_New()
{
// -----
// ARRANGE
Builder.WorkflowDefaultSetup.Load(Container);
Builder.ProjectDefaultSetup.Load(Container);
var project = new Project();
project.ProjectName = "Red Barchetta";
project.Description = "To keep it as new has been our dearest dream";
// -----
// ACT
var provider = Container.GetRequiredService<ProjectProvider>();
provider.CreateProject(project);
// -----
// ASSERT
Assert.IsTrue(project.Id > 0);
Assert.IsNotNull(project.ObjectState);
Assert.IsInstanceOfType(project.ObjectState, typeof(Bushido.Project.Business.StateManagement.Project.New));
}
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;
}
I'm utilizing Entity Framework 4.3 Migrations in my project. I would like to use Automatic migrations so that when I make modifications to my domain objects and my context class, my database automatically updates when I run the project. I have this working so far.
I would also like to use some Added Migrations in addition to the automatic migrations, and I would like the application to automatically jump to the latest version (based on my added migrations) when I run the application.
In order to do this I have placed this in the global.asax file...
Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Core.Migrations.Configuration>());
Now this works, but when I do this it no longer automatically updates the database based on my domain objects.
I would like to be able to completely delete the database and then run the application and have all the automatic migrations run and then have my explicit migrations run and bring the database up to the latest version.
I know I've had this working in a previous project, but I'm not sure what I'm doing wrong in this instance.
Thanks
You need to pass a configuration that has the AutomaticMigrationsEnabled set to true in the constructor. Something like this should help:
Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, MyConfiguration>());
with MyConfiguration being something like:
public class MyConfiguration : Core.Migrations.Configuration
{
public MyConfiguration { this.AutomaticMigrationsEnabled = true; }
}
DISCLAIMER: Just hacked this in, so small tweaks might be required to get this to compile
EDIT:
Just checked with EF 4.3.1 and the code is like this for the initializer:
Database.SetInitializer(new MigrateDatabaseToLatestVersion<DataContext, MyConfiguration>());
and this for the configuration class:
public class MyConfiguration : System.Data.Entity.Migrations.DbMigrationsConfiguration<DataContext>
{
public MyConfiguration()
{
this.AutomaticMigrationsEnabled = true;
}
}
After banging my head on this for several hours, I finally came up with a solution that creates the database if necessary or upgrades it if out of date. We use this technique in Gallery Server Pro to make it easy to install the first time or upgrade previous versions.
private static void InitializeDataStore()
{
System.Data.Entity.Database.SetInitializer(new System.Data.Entity.MigrateDatabaseToLatestVersion<GalleryDb, GalleryDbMigrationConfiguration>());
var configuration = new GalleryDbMigrationConfiguration();
var migrator = new System.Data.Entity.Migrations.DbMigrator(configuration);
if (migrator.GetPendingMigrations().Any())
{
migrator.Update();
}
}
public sealed class GalleryDbMigrationConfiguration : DbMigrationsConfiguration<GalleryDb>
{
protected override void Seed(GalleryDb ctx)
{
MigrateController.ApplyDbUpdates();
}
}
I wrote up a blog post with a few more details:
Using Entity Framework Code First Migrations to auto-create and auto-update an application
Same solution that Roger did but using an static constructor on the DbContext.
Full code below.... this allow the initialization code to live on the class itself and is self-invoked at the first instantiation of the DataDbContext class.
public partial class DataDbContext : DbContext
{
public DataDbContext()
: base("name=DefaultConnection")
{
}
static DataDbContext() // This is an enhancement to Roger's answer
{
Database.SetInitializer(new DataDbInitializer());
var configuration = new DataDbConfiguration();
var migrator = new DbMigrator(configuration);
if (migrator.GetPendingMigrations().Any())
migrator.Update();
}
// DbSet's
public DbSet<CountryRegion> CountryRegion { get; set; }
// bla bla bla.....
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
Configuration.ProxyCreationEnabled = false;
Configuration.LazyLoadingEnabled = false;
//Configuration.ValidateOnSaveEnabled = false;
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly()); // Discover and apply all EntityTypeConfiguration<TEntity> of this assembly, it will discover (*)
}
}
internal sealed class DataDbInitializer : MigrateDatabaseToLatestVersion<DataDbContext, DataDbConfiguration>
{
}
internal sealed class DataDbConfiguration : DbMigrationsConfiguration<DataDbContext>
{
public DataDbConfiguration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(DataDbContext context)
{
DataSeedInitializer.Seed(context);
base.Seed(context);
}
}
internal static class DataSeedInitializer
{
public static void Seed(DataDbContext context)
{
SeedCountryRegion.Seed(context);
// bla bla bla.....
context.SaveChanges();
}
}
internal static class SeedCountryRegion
{
public static void Seed(DataDbContext context)
{
context.CountryRegion.AddOrUpdate(countryRegion => countryRegion.Id,
new CountryRegion { Id = "AF", Name = "Afghanistan" },
new CountryRegion { Id = "AL", Name = "Albania" },
// bla bla bla.....
new CountryRegion { Id = "ZW", Name = "Zimbabwe" });
context.SaveChanges();
}
}
public class CountryRegionConfiguration : EntityTypeConfiguration<CountryRegion> // (*) Discovered by
{
public CountryRegionConfiguration()
{
Property(e => e.Id)
.IsRequired()
.HasMaxLength(3);
Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
}
}
public partial class CountryRegion : IEntity<string>
{
// Primary key
public string Id { get; set; }
public string Name { get; set; }
}
public abstract class Entity<T> : IEntity<T>
{
//Primary key
public abstract T Id { get; set; }
}
public interface IEntity<T>
{
T Id { get; set; }
}
We can see that the Seed method is running again and again..
We can avoid this by checking if a migration already exits, since one is applied automatically when the database is create.. then we can refactor the DataDbConfiguration as follows...
internal sealed class DataDbConfiguration : DbMigrationsConfiguration<DataDbContext>
{
private readonly bool _isInitialized;
public DataDbConfiguration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
var migrator = new DbMigrator(this);
_isInitialized = migrator.GetDatabaseMigrations().Any();
}
protected override void Seed(DataDbContext context)
{
InitializeDatabase(context);
}
public void InitializeDatabase(DataDbContext context)
{
if (!_isInitialized)
{
if (context.Database.Connection.ConnectionString.Contains("localdb"))
{
DataSeedInitializer.Seed(context); // Seed Initial Test Data
}
else
{
// Do Seed Initial Production Data here
}
}
else
{
// Do any recurrent Seed here
}
}
}
Here is my current solution, which I'm not completely satisfied with.
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
var context = new KCSoccerDataContext();
var initializeDomain = new CreateDatabaseIfNotExists<KCSoccerDataContext>();
var initializeMigrations = new MigrateDatabaseToLatestVersion<KCSoccerDataContext, Core.Migrations.Configuration>();
initializeDomain.InitializeDatabase(context);
initializeMigrations.InitializeDatabase(context);
}
I'm actually creating two different initializers. The first, using CreateDatabaseIfNotExists, succcessfully goes through and creates tables based on my Domain objects. The second, using MigrateDatabaseToLatestVersion, executes all of my explicit migrations.
I don't like it because Automatic Migrations are basically disabled. So in order to add or change my Domain model I have to completely drop the database and recreate it. This won't be acceptable once I've moved the application to production.
If your application contains Startup.cs class, you can use DbMigrator Class as follows
Go to your App_Start folder, open Startup.Auth
Paste these lines of code inside of ConfigureAuth method
var configuration = new Migrations.Configuration();
var dbmigrator = new DbMigrator(configuration);
dbmigrator.Update();
NOTE: Remember to use this namespace- using System.Data.Entity.Migrations;
what this does is to update your database to the latest version anytime the application starts up
You just need to do
private static void InitializeDataStore()
{
System.Data.Entity.Database.SetInitializer(new System.Data.Entity.MigrateDatabaseToLatestVersion<GalleryDb, GalleryDbMigrationConfiguration>());
System.Data.Entity.Database.Initialize(false);
}