Flyway: multiple databases with same migration version number errors out - flyway

Baselining two databases on the same server, with each having their own starting migration script numbered V2__, I get the error below:
flyway:baseline produces the error
Found more than one migration with version 2
Each migration script lives in its own migration dir: db/migration/db1, db/migration/db2
Did I miss a config, or is this expected?

I learned its not possible to migrate multiple databases using just property files. Its only possible by triggering the migration with Java code, so I wired up a bean to handle this:
#Configuration
public class FlywaySlaveInitializer {
#Autowired
Environment env;
#Bean(initMethod = "migrate")
#PostConstruct
Flyway flyway() {
String datasource = env.getProperty("flyway.url");
String[] schemas = env.getProperty("flyway.schemas").split(",");
String[] schemaLocations = env.getProperty("flyway.locations").split(",");
Flyway flyway = new Flyway();
for (int schema = 0; schema < schemas.length; schema++) {
flyway.setDataSource(datasource + "/" + schemas[schema], "root", "root");
flyway.setSchemas(schemas[schema]);
flyway.setLocations("db/migration/" + schemaLocations[schema]);
flyway.migrate();
}
return flyway;
}
}

Related

ASP.NET MVC CORE web application integration tests with EF Core in-memory database - fresh database for each test

I am learning about ASP.NET Core 3 and have built a basic application. I am looking run integration tests to assert calls to the controllers read/write from the database correctly. To avoid having to rely on the actual database I am looking at using EF Core's in-memory database. I have been following this article as my main guide.
The problem I have is that I am struggling to ensure each separate integration test uses a fresh database context.
Initially, I encountered errors calling my database seed method more than once (the second and subsequent calls failed to add a duplicate primary key - essentially it was using the same context).
From looking at various blogs, tutorial and other questions here, I worked around this by instantiating the in-memory database with a unique name (using Guid.NewGuid()). This should have solved my problem. However, this gave me a different issue. The database seed method was correctly called at each test initialisation, however when I then called a controller action the dependency injection instantiated a new database context, meaning that no seed data was present!
I seem to be going in circles either only being able to call seed data once, and only being able to have a single test, or having more than one test but with no seed data!
I have experimented with the scope lifetimes for the DbContext service, setting this to transient/scoped/singleton, but with seemingly no difference in results.
The only way I have managed to get this to work is to add a call to db.Database.EnsureDeleted() before the call to db.Database.EnsureCreated() in the seed method, but this seems like a massive hack and doesn't feel right.
Posted below is my utilities class to set up the in-memory database for the tests, and a test class. Hopefully this is sufficient, as I feel this post is long enough as it is, but the actual controller / startup class can be posted if necessary (though they are fairly vanilla).
Any help much appreciated.
Utilities class to set up the in-memory database
using CompetitionStats.Entities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
namespace CompetitionStatsUnitTests
{
class Utilities
{
internal class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the app's ApplicationDbContext registration.
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<CompetitionStatsContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// Add ApplicationDbContext using an in-memory database for testing.
services.AddDbContext<CompetitionStatsContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<CompetitionStatsContext>();
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
db.Database.EnsureDeleted(); // feels hacky - don't think this is good practice, but does achieve my intention
db.Database.EnsureCreated();
try
{
InitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding the database with test messages. Error: {Message}}", ex.Message);
}
}
});
}
private static void InitializeDbForTests(CompetitionStatsContext db)
{
db.Teams.Add(new CompetitionStats.Models.TeamDTO
{
Id = new Guid("3b477978-f280-11e9-8490-a8667f2f93c4"),
Name = "Arsenal"
});
db.SaveChanges();
}
}
}
}
Test class
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net.Http;
using System.Threading.Tasks;
namespace CompetitionStatsUnitTests.ControllerUnitTests
{
[TestClass]
public class TeamControllerTest
{
private HttpClient _testClient;
[TestInitialize]
public void Initialize()
{
var factory = new Utilities.CustomWebApplicationFactory<CompetitionStats.Startup>();
this._testClient = factory.CreateClient();
}
[TestMethod]
public async Task TeamController_GetTeam_Returns_Team()
{
var actualResponse = await this._testClient.GetStringAsync("api/teams/3b477978-f280-11e9-8490-a8667f2f93c4");
var expectedResponse = #"{""id"":""3b477978-f280-11e9-8490-a8667f2f93c4"",""name"":""Arsenal""}";
Assert.AreEqual(expectedResponse, actualResponse);
}
[TestMethod]
public async Task TeamController_PostTeam_Adds_Team()
{
var content = new StringContent(#"{""Name"": ""Liverpool FC""}", System.Text.Encoding.UTF8, "application/json");
var response = await this._testClient.PostAsync("api/teams/", content);
Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.Created);
}
}
}
options.UseInMemoryDatabase("InMemoryDbForTesting");
This creates/uses a database with the name “MyDatabase”. If UseInMemoryDatabase is called again with the same name, then the same in-memory database will be used, allowing it to be shared by multiple context instances.
So you will get the error like{"An item with the same key has already been added. Key: 3b477978-f280-11e9-8490-a8667f2f93c4"} when you add data with the same Id repeatedly
You could add a judgment to the initialization method :
private static void InitializeDbForTests(CompetitionStatsContext db)
{
if (!db.Teams.Any())
{
db.Teams.Add(new Team
{
Id = new Guid("3b477978-f280-11e9-8490-a8667f2f93c4"),
Name = "Arsenal"
});
}
db.SaveChanges();
}
You could also refer to the suggestions provided by Grant says adios SE in this thread

How to enable migration in EntityFramework Core Sqlite

I have Dbset and creating database like this. When first time db is created then after I add column in dbset, then the migration does not work the table column is not found. Please help me how I can enable EF Core Sqlite migrations in Xamarin Forms.
public BaseSQLRespository(string databasePath)
{
try
{
_databasePath = databasePath;
Database.EnsureCreated();
// bool IsDatabaseCreated= ;
// Settings.IsDatabaseCreatedSettings = IsDatabaseCreated;
}
catch (Exception ex)
{
throw ex;
}
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
try
{
optionsBuilder.UseSqlite(string.Format("Filename={0}", _databasePath));
}
catch (Exception ex)
{
throw ex;
}
}
Assuming you created a migration for your new column using the add-migration command, you can enable the execution of migrations at runtime by calling the Migrate() method instead of EnsureCreated(). The docs for the method can be found here.
As per Microsoft's documentation, the Migrate method is incompatible with the EnsureCreated method which actually bypasses the migrations when creating the database schema - so in your case you will have to delete the old database (uninstall the app or clear it's data) before trying the new code.
Don't call EnsureCreated() before Migrate(). EnsureCreated() bypasses Migrations to create the schema, which causes Migrate() to fail.
Source: EF Core - Apply migrations at runtime
To add an initial migration, run the following command:
dotnet ef migrations add YourNameOfMigration
Next, apply the migration to the database to create the schema:
dotnet ef database update
Migrations
Every time when you add a new field to the model you should add migration and apply it to DB.
i guess your configuration is wrong, you shouldn't call Database.EnsureCreated(); in ctor because OnConfiguring() is not called yet and your database is not initialized.
you can create a method which called once your serviceProvider is started.
public static void Initialize(IServiceProvider serviceProvider)
{
var context = serviceProvider.GetRequiredService<BaseSQLRespository>();//use your service provider any thing which you can get your current created `BaseSQLRespository`
context.Database.EnsureCreated();//once call when your app is started. (it is up to you which method you used to call)
...
...
}
In Addition, here is similar topic and different solution,
you can check: Error in Database.EnsureCreated() while using Entity Framework with Sqlite in Xamarin

AspNet Core Add Migration - ConnectionString is from database

I am working with AspNet Core 2.0, I have a situation where I have a context but the connection string is dynamic as per selected by the user at runtime.
Add-Migration doesn't work as it wants to have the connection string to match migration history.
var datastore = _MainDBContext.datastores.FirstOrDefault(x=>x.db_type=="MS");
string connectionString = #"Server=" + datastore.db_instance_ip + ";Port=3306;Database=" + datastore.db_name + ";Uid=" + datastore.db_user + ";Password=" + datastore.db_pass + ";";
optionsBuilder.UseMySQL(connectionString);
_MSDBContext= new MSDBContext(optionsBuilder.Options);
_MSDBContext.Database.Migrate();
Error
No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.
I want the migrate to create the database along with tables dynamically. Any help is appreciated.
Thanks
The error message says your DbContext needs a constructor which accepts a DbContextOptions.
public MSDBContext(DbContextOptions options) : base(options)
{
}
Or you could try the following approach (inside of your context class):
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
var connectionString = configuration.GetConnectionString("NAME_OF_CONNECTION_STRING");
optionsBuilder.UseSqlServer(connectionString); //or any other DB provider
}
}
Of course, you can use different logic to get the connection string. You don't need to use appsettings.json file.

How to upgrade from flyway 3 directly to flyway 5

Working on a product that is deployed by many clients in many production environments. It includes at least one Spring Boot app.
We've used flyway for db schema migrations. Upgrading from Spring Boot 1.5.x to 2.0.x has bumped our flyway version from 3.x to 5.x.
The Spring Boot migration guide simply says to upgrade to flyway 4 before the boot upgrade. However, this would require all of our customers to do an intermediate upgrade before being able to upgrade to the latest.
So, the question is: How would you upgrade from flyway 3 directly to flyway 5?
Step 0.
Upgrade to spring boot v2.1 (and therby implicitly to flyway 5).
Step 1.
Since schema_version was used in flyway 3.x let new flyway versions know that they should keep using this table.:
# application.yml
spring.flyway.table: schema_version # prior flyway version used this table and we keep it
Step 2.
Create file src/main/ressources/db/migration/flyway_upgradeMetaDataTable_V3_to_V4.sql for upgrading the meta table based on the dialect you use.
See https://github.com/flyway/flyway/commit/cea8526d7d0a9b0ec35bffa5cb43ae08ea5849e4#diff-b9cb194749ffef15acc9969b90488d98 for the update scripts of several dialects.
Here is the one for postgres and assuming the flyway table name is schema_version:
-- src/main/ressources/db/migration/flyway_upgradeMetaDataTable_V3_to_V4.sql
DROP INDEX "schema_version_vr_idx";
DROP INDEX "schema_version_ir_idx";
ALTER TABLE "schema_version" DROP COLUMN "version_rank";
ALTER TABLE "schema_version" DROP CONSTRAINT "schema_version_pk";
ALTER TABLE "schema_version" ALTER COLUMN "version" DROP NOT NULL;
ALTER TABLE "schema_version" ADD CONSTRAINT "schema_version_pk" PRIMARY KEY ("installed_rank");
UPDATE "schema_version" SET "type"='BASELINE' WHERE "type"='INIT';
Step 3.
Create Java file your.package/FlywayUpdate3To4Callback.java
Please note that this does the following:
Run the sql script from Step 2
call Flyway.repair()
// FlywayUpdate3To4Callback.java
package your.package;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
import org.flywaydb.core.api.configuration.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
#Component
#Order(HIGHEST_PRECEDENCE)
#Slf4j
public class FlywayUpdate3To4Callback implements Callback {
private final Flyway flyway;
public FlywayUpdate3To4Callback(#Lazy Flyway flyway) {
this.flyway = flyway;
}
private boolean checkColumnExists(Configuration flywayConfiguration) throws MetaDataAccessException {
return (boolean) JdbcUtils.extractDatabaseMetaData(flywayConfiguration.getDataSource(),
callback -> callback
.getColumns(null, null, flywayConfiguration.getTable(), "version_rank")
.next());
}
#Override
public boolean supports(Event event, Context context) {
return event == Event.BEFORE_VALIDATE;
}
#Override
public boolean canHandleInTransaction(Event event, Context context) {
return false;
}
#Override
public void handle(Event event, Context context) {
boolean versionRankColumnExists = false;
try {
versionRankColumnExists = checkColumnExists(context.getConfiguration());
} catch (MetaDataAccessException e) {
log.error("Cannot obtain flyway metadata");
return;
}
if (versionRankColumnExists) {
log.info("Upgrading metadata table the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("db/migration/common/flyway_upgradeMetaDataTable_V3_to_V4.sql",
Thread.currentThread().getContextClassLoader());
ScriptUtils.executeSqlScript(context.getConnection(), resource);
log.info("Flyway metadata table updated successfully.");
// recalculate checksums
flyway.repair();
}
}
}
Step 4.
Run spring boot.
The log should show info messages similar to these:
...FlywayUpdate3To4Callback : Upgrading metadata table the Flyway 4.0 format
...FlywayUpdate3To4Callback : Flyway metadata table updated successfully.
Credits
This answer is based on Eduardo Rodrigues answer by changing:
Use Event.BEFORE_VALIDATE to trigger a flyway callback that upgrade flyway 3 to 4.
more information on application.yml setup
provide upgrade sql migration script
In case I'm not the last person on the planet to still be upgrading from 3 to 5.
Problem:
I wanted the upgrade to be transparent to other developers on the project as well as not requiring any special deployment instructions when upgrading on the live applications, so I did the following.
I had a look at how version 4 handled the upgrade:
In Flyway.java a call is made to MetaDataTableImpl.upgradeIfNecessary
upgradeIfNecessary checks if the version_rank column still exists, and if so runs a migration script called upgradeMetaDataTable.sql from org/flywaydb/core/internal/dbsupport/YOUR_DB/
If upgradeIfNecessary executed, then Flyway.java runs a DbRepair calling repairChecksumsAndDescriptions
This is easy enough to do manually but to make it transparent. The app is a spring app, but not a spring boot app, so at the time I had flyway running migrations automatically on application startup by having LocalContainerEntityManager bean construction dependent on the flyway bean, which would call migrate as its init method (explained here Flyway Spring JPA2 integration - possible to keep schema validation?), so the order of bootstrapping would be:
Flyway bean created -> Flyway migrate called -> LocalContainerEntityManager created
Solution:
I changed the order of bootstrapping to:
Flyway bean created -> Flyway3To4Migrator -> LocalContainerEntityManager created
where Flyway3To4Migrator would perform the schema_table changes if needed, run the repair if the upgrade happened, and then always run flyway.migrate to continue the migrations.
#Configuration
public class AppConfiguration {
#Bean
// Previously: #DependsOn("flyway")
#DependsOn("flyway3To4Migrator")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
...
}
// Previously: #Bean(initMethod = "migrate")
#Bean
public Flyway flyway(DataSource dataSource) {
...
}
}
#Component
#DependsOn("flyway")
public class Flyway3To4Migrator {
private final Log logger = LogFactory.getLog(getClass());
private Flyway flyway;
#Autowired
public Flyway3To4Migrator(Flyway flyway) {
this.flyway = flyway;
}
#PostConstruct
public void migrate() throws SQLException, MetaDataAccessException {
DataSource dataSource = flyway.getDataSource();
boolean versionRankColumnExists = checkColumnExists(dataSource);
if (versionRankColumnExists) {
logger.info("Upgrading metadata table to the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("upgradeMetaDataTable.sql", getClass().getClassLoader());
ScriptUtils.executeSqlScript(dataSource.getConnection(), resource);
logger.info("Metadata table successfully upgraded to the Flyway 4.0 format.");
logger.info("Running flyway:repair for Flyway upgrade.");
flyway.repair();
logger.info("Complete flyway:repair.");
}
logger.info("Continuing with normal Flyway migration.");
flyway.migrate();
}
private boolean checkColumnExists(DataSource dataSource) throws MetaDataAccessException {
return (Boolean) JdbcUtils.extractDatabaseMetaData(
dataSource, dbmd -> {
ResultSet rs = dbmd.getColumns(
null, null,
"schema_version",
"version_rank");
return rs.next();
});
}
}
A few things to note:
At some point we will remove the extra Flyway3To4Migrator class and revert the configuration to the way it was.
I copied the relevant upgradeMetaDataTable.sql file for my database from the v4 Flyway jar and simplified it to my table names etc. You could grab the schema and table names from flyway if you needed to.
there is no transaction management around the SQL script, you might want to add that
Flyway3To4Migrator calls flyway.repair(), which does a little more than DbRepair.repairChecksumsAndDescriptions(), but we were happy to accept the database must be in a good state before its run
If you're using Spring Boot, you can register a callback that does the upgrade on beforeMigrate(). The code is similar to #trf and looks like this:
#Component
#Order(HIGHEST_PRECEDENCE)
#Slf4j
public class FlywayUpdate3To4Callback extends BaseFlywayCallback {
private final Flyway flyway;
public FlywayUpdate3To4Callback(#Lazy Flyway flyway) {
this.flyway = flyway;
}
#Override
public void beforeMigrate(Connection connection) {
boolean versionRankColumnExists = false;
try {
versionRankColumnExists = checkColumnExists(flywayConfiguration);
} catch (MetaDataAccessException e) {
log.error("Cannot obtain flyway metadata");
return;
}
if (versionRankColumnExists) {
log.info("Upgrading metadata table the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("upgradeMetaDataTable.sql",
Thread.currentThread().getContextClassLoader());
ScriptUtils.executeSqlScript(connection, resource);
log.info("Flyway metadata table updated successfully.");
// recalculate checksums
flyway.repair();
}
}
private boolean checkColumnExists(FlywayConfiguration flywayConfiguration) throws MetaDataAccessException {
return (boolean) JdbcUtils.extractDatabaseMetaData(flywayConfiguration.getDataSource(),
callback -> callback
.getColumns(null, null, flywayConfiguration.getTable(), "version_rank")
.next());
}
Notice that you don't need to manually call flyway.migrate() here.
The code above is not compatible with version 5. It uses deprecated classes.
Here is an updated version.
import lombok.extern.slf4j.Slf4j;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
import org.flywaydb.core.api.configuration.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.stereotype.Component;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
#Component
#Order(HIGHEST_PRECEDENCE)
#Slf4j
public class FlywayUpdate3To4Callback implements Callback {
private final Flyway flyway;
public FlywayUpdate3To4Callback(#Lazy Flyway flyway) {
this.flyway = flyway;
}
private boolean checkColumnExists(Configuration flywayConfiguration) throws MetaDataAccessException {
return (boolean) JdbcUtils.extractDatabaseMetaData(flywayConfiguration.getDataSource(),
callback -> callback
.getColumns(null, null, flywayConfiguration.getTable(), "version_rank")
.next());
}
#Override
public boolean supports(Event event, Context context) {
return event == Event.BEFORE_VALIDATE;
}
#Override
public boolean canHandleInTransaction(Event event, Context context) {
return false;
}
#Override
public void handle(Event event, Context context) {
boolean versionRankColumnExists = false;
try {
versionRankColumnExists = checkColumnExists(context.getConfiguration());
} catch (MetaDataAccessException e) {
log.error("Cannot obtain flyway metadata");
return;
}
if (versionRankColumnExists) {
log.info("Upgrading metadata table the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("db/migration/flyway_upgradeMetaDataTable_V3_to_V4.sql",
Thread.currentThread().getContextClassLoader());
ScriptUtils.executeSqlScript(context.getConnection(), resource);
log.info("Flyway metadata table updated successfully.");
// recalculate checksums
flyway.repair();
}
}
}
I tried to skip v4 too but didn't work. Running the repair from 3 to 5 will make the checksums correct, but won't change the schema_version format. That has changed as well.
It seems you need to go to v4 first. Even if temporarily just to run mvn flyway:validate, which will repair schema_version.
I've done this on this repo: https://github.com/fabiofalci/flyway-from-3-to-5/commits/5.0.7
The first commit is v3, the second commit is v4 (where I ran validate) and then the third commit on v5 the schema is correct.
It worked for me, except I had to put Event.BEFORE_VALIDATE again instead of Event.BEFORE_MIGRATE that was present in the last version of the FlywayUpdate3To4Callback Class. This is because I had a checksum not valid on a migration already run, so it needed to be fixed before validating and not before migrating. Thanks.

How and where to call Database.EnsureCreated and Database.Migrate?

I have a ASP.NET MVC 6 application, and i need to call the Database.EnsureCreated and Database.Migrate methods.
But where should I call them?
I think this is an important question and should be well answered!
What is Database.EnsureCreated?
context.Database.EnsureCreated() is new EF core method which ensures that the database for the context exists. If it exists, no action is taken. If it does not exist then the database and all its schema are created and also it ensures it is compatible with the model for this context.
Note:
This method does not use migrations to create the database. In addition, the database that is created cannot later be updated using migrations. If you are targeting a relational database and using migrations, you can use the DbContext.Database.Migrate() method to ensure the database is created and all migrations are applied.
How did we do that with EF 6?
context.Database.EnsureCreated() is equivalent to the below listed approaches of EF 6:
Package Manager Console:
Enable-Migrations -EnableAutomaticMigrations. Add-Migration/Update-Database.
From code:
Database.SetInitializer CreateDatabaseIfNotExists
or
With DbMigrationsConfiguration and set AutomaticMigrationsEnabled = true;
What is Database.Migrate?
Applies any pending migrations for the context to the database. Will create the database if it does not already exist.
How did we do that with EF 6?
context.Database.Migrate() is equivalent to the below listed approaches of EF 6:
Package Manager Console:
Update-Database -TargetMigration
With a custom DbMigrationsConfiguration:
AutomaticMigrationsEnabled = false; or with DbMigrator.
Conclusion:
If you are using migrations there is context.Database.Migrate(). If you don't want migrations and just want a quick database (usually for testing) then use context.Database.EnsureCreated()/EnsureDeleted().
With the information that James P and Bassam Alugili provided, what I ended up doing was to add these lines of code to the Configure method in the Startup class (Startup.cs):
using (var scope =
app.ApplicationServices.CreateScope())
using (var context = scope.ServiceProvider.GetService<MyDbContext>())
context.Database.Migrate();
Ordinarily, the DbContext will be added to the dependency injection container in Startup.ConfigureServices() like so:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add DbContext to the injection container
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(
this.Configuration.GetConnectionString("DefaultConnection")));
}
}
However, the IServiceCollection doesn't act as a service provider, and since the DbContext was not registered with the injection container before the current scope (Startup.ConfigureServices), we can't access the context through dependency injection here.
Henk Mollema discusses manually resolving services during startup here, but mentions that...
manually resolving services (aka Service Locator) is generally
considered an anti-pattern ... [and] you should avoid it as much
as possible.
Henk also mentions that the Startup constructor's dependency injection is very limited and does not include services configured in Startup.ConfigureServices(), so DbContext usage is easiest and most appropriate through the injection container used throughout the rest of the app.
The runtime's hosting service provider can inject certain services into the constructor of the Startup class, such as IConfiguration, IWebHostEnvironment (IHostingEnvironment in pre-3.0 versions), ILoggerFactory and IServiceProvider. Note that the latter is an instance built by the hosting layer and contains only the essential services for starting up an application.
In order to call Database.EnsureCreated() or Database.Migrate(), we can, and want to, have the DbContext resolve automatically in Startup.Configure(), where our configured services are now available through DI:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add DbContext to the injection container
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(
this.Configuration.GetConnectionString("DefaultConnection")));
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyDbContext context)
{
if (env.IsDevelopment())
{
context.Database.EnsureCreated();
//context.Database.Migrate();
}
}
}
Please remember as Bassam Alugili's answer referenced from EF Core documentation that Database.EnsureCreated() and Database.Migrate() are not meant to be used together because one ensures your existing migrations are applied to the database, which is created if needed. The other just ensures a database exists, and if not, creates one that reflects your DbContext, including any seeding done through the Fluent API in the context.
Just as a foreward you should read this from Rowan Miller:
... EnsureCreated totally bypasses migrations and just creates the
schema for you, you can't mix this with migrations. EnsureCreated is
designed for testing or rapid prototyping where you are ok with
dropping and re-creating the database each time. If you are using
migrations and want to have them automatically applied on app start,
then you can use context.Database.Migrate() instead.
According to answer here you need to add Globals.EnsureDatabaseCreated(); it to Startup.cs:
Startup function in Startup.cs:
public Startup(IHostingEnvironment env)
{
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
if (env.IsDevelopment())
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
Configuration = builder.Build();
Globals.Configuration = Configuration;
Globals.HostingEnvironment = env;
Globals.EnsureDatabaseCreated();
}
And define Globals.EnsureDatabaseCreated() as follows:
public static void EnsureDatabaseCreated()
{
var optionsBuilder = new DbContextOptionsBuilder();
if (HostingEnvironment.IsDevelopment()) optionsBuilder.UseSqlServer(Configuration["Data:dev:DataContext"]);
else if (HostingEnvironment.IsStaging()) optionsBuilder.UseSqlServer(Configuration["Data:staging:DataContext"]);
else if (HostingEnvironment.IsProduction()) optionsBuilder.UseSqlServer(Configuration["Data:live:DataContext"]);
var context = new ApplicationContext(optionsBuilder.Options);
context.Database.EnsureCreated();
optionsBuilder = new DbContextOptionsBuilder();
if (HostingEnvironment.IsDevelopment()) optionsBuilder.UseSqlServer(Configuration["Data:dev:TransientContext"]);
else if (HostingEnvironment.IsStaging()) optionsBuilder.UseSqlServer(Configuration["Data:staging:TransientContext"]);
else if (HostingEnvironment.IsProduction()) optionsBuilder.UseSqlServer(Configuration["Data:live:TransientContext"]);
new TransientContext(optionsBuilder.Options).Database.EnsureCreated();
}
To use context.Database.Migrate() see here or here.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<YourDbContext>(option => option.UseSqlServer(#"Data source=(localdb)\ProjectModels;Initial Catalog=YourDb;Integrated Security=True"));
var app = builder.Build();
// Configure the HTTP request pipeline.
YourDbContext dbcontext = app.Services.GetRequiredService<YourDbContext>();
dbcontext.Database.EnsureCreated();
Additionally you may see a performance hit if you call this in the constructor of your context... After moving EnsureCreated to the setup.cs utility, I noticed considerable improvements to my response times.
Note: I am using EFC and UWP.
If you are working with VS 2022 / .Net Version 6 and you are trying to find a way to create your database..then
Do these following steps
Add Microsoft.EntityFramework.Tools reference through Package manager
from Package Manager Console
Run Step 1
Add-Migration InitialMigration
InitialMigration here is custom name you can type anything you want..
let it run
Step 2
Update-Database
This should create your database.
I am developing ASP .NET Core 6 Web API.
In Program.cs after building the app I wrote the code below.
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
using var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
context.Database.EnsureCreated();
}
context.Database.EnsureCreated() ensures that the database for the context exists.
If the database exists and has any tables, then no action is taken. Nothing is done to ensure the database schema is compatible with the Entity Framework model.
If the database exists but does not have any tables, then the Entity Framework model is used to create the database schema.
If the database does not exist, then the database is created and the Entity Framework model is used to create the database schema.
I hope I helped.

Resources