Have migrations and a service that needs the database? - c#

How do I build a service that needs access to my database while also using migrations, given that the migrations tool "dotnet ef database update" runs my application BuildWebHost before creating the database? The attempt to configure the service needing the database throws an exception because the database doesn't exist and causes the migration command to fail. The database therefore never gets created.
I'm using asp.net core 2 and EF Core 2.
More specifically, running "dotnet ef database update" with a blank database fails with the following error:
An error occurred while calling method 'BuildWebHost' on class
'Program'.Continuing without the application service provider. Error:
Cannot open database "MyDb" requested by the login. The login failed.
Login failed for user 'MYCOMPUTER\MYNAME'.
This happens because I have built a custom configuration provider backed by my "MyDb" database (with the end goal of binding to it with a custom options class) as per this Microsoft Configuration Tutorial My program.cs looks like:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((builderContext, config) =>
{
var tmpconfig = config.Build();
config.AddMyOptionsConfig(options => options.UseSqlServer(tmpconfig.GetConnectionString("My_Database")));
})
.UseStartup<Startup>()
.Build();
and the AddMyOptionsConfig eventually runs:
public class EFConfigProvider : ConfigurationProvider
{
[...]
// Load config data from EF DB.
public override void Load()
{
var builder = new DbContextOptionsBuilder<MyDbContext>();
OptionsAction(builder);
using (var dbContext = new MyDbContext(builder.Options))
{
// dbContext.Database.EnsureCreated(); // will cause first migration to fail
Data = !dbContext.ConfigurationValue.Any() // throws exception
? CreateAndSaveDefaultValues(dbContext)
: dbContext.ConfigurationValue.ToDictionary(c => c.Id, c => c.Value);
which throws an SQLException when it attempts to access the database because the database hasn't been created yet.
However, if I try to use dbContext.Database.EnsureCreated() then the initial migration fails because the tables already exist. I thought of trying dbContext.Database.Migrate() instead but as a beginner I'm concerned there might be unintended consequences for a production environment. As such I'd prefer to have control over migrations via the command line tools.
Fundamentally, the problem seems to be that "dotnet ef database update" runs the application startup - BuildWebHost - before it creates the database but the custom configuration provider added in BuildWebHost needs the database to already exist.
How do I solve this dilemma?

In a sense, you've created a circular reference. When running migrations on a ASP.NET Core project, the application is initialized to instantiate the DbContext needed to run migrations against. This is due to the fact that DbContext in EF Core now requires a DbContextOptions instance to be injected, as opposed to the old way of doing things in EF, where the connection string name (or the actual full connection string) would be defined directly on the constructor.
Normally, this would work just fine, but, as you've noticed, because the application initialization, itself, requires an already existing database, there's no way to run this before the migrations. As a result, you have two options:
You can attempt to abstract the piece that requires an existing database. That could be as simple as wrapping it in a try-catch and swallowing the exception, or more complex. Since the migration piece doesn't need this particular functionality, it could safely be excluded from the application initialization in that scenario.
You can move your context and entities into a class library and implement an IDesignTimeDbContextFactory to satisfy the ability to construct a DbContext to migrate against. You would then run the migrations against this class library instead of your ASP.NET Core project. That then sidesteps the issue of having to initialize the application in order to do a migration.

BuildWebHost (and Startup.Configure) shouldn't be used for application startup logic. The guidance from the ASP.NET team is to use Program.Main instead.

My solution so far has been to move the entity backing the custom configuration provider into a separate dbContext.
Now the table can be created when needed via MyDbOptionsContext.Database.EnsureCreated() without affecting the migrations, as they are based on the main context (MyDbContext).
The options table no longer participates in migrations but as it's unlikely to change there shouldn't be a problem. The other downside is the need (due to having 2 contexts) to explicitly specify the context for the dotnet ef command line tool.

Related

C# Entity Framework error when MSSQL server has View any database = denied

We use MSSQL for our C# .NET Framework 4.8 Application using Entity Framework for database related activities.
But on our production environment the SQL server has the Securable: View any database on Deny.
The database for the application exists but Entity Framework cannot see the database and tries to create it, this results in the CREATE DATABASE permission denied in database 'master' error.
I am using CreateDatabaseIfNotExists and MigrateDatabaseToLatestVersion in my Application_Start().
Now the issue (I think) lies with CreateDatabaseIfNotExists.
For the first run we give the db user enough rights to create and fill the database, it does this without problem.
But after the initial setup we remove those rights and the issue starts.
It tries to create the database, But it already exists.
And I am hoping there is a way to have both Automatic database creation/migration, and the View any database on deny securable.
Does anyone have a idea on how to solve this issue?
Is there some sort of option I could enable to stop this behaviour?
You should "wire in" IHostingEnvironment and make sure you run
CreateDatabaseIfNotExists and MigrateDatabaseToLatestVersion
only in certain environments.
===========
For DotNet-Core (NON asp.net-core) apps.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.internal.hostingenvironment?view=dotnet-plat-ext-7.0
for asp.net-core.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment?view=aspnetcore-6.0
....
Then you will use (probably an existing)
"Is" method:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingenvironmentextensions.isdevelopment?view=aspnetcore-7.0
IsDevelopment
IsProduction
IsStaging
or you have the ability to "add your own environment".. with
IsEnvironment(string)
I would NEVER leave to "auto-voodoo" what might happen to the production database.
You can see this approach:
https://stackoverflow.com/a/60399887/214977

Multiple DbConfigurations with different execution strategies in EF6

We've been using EF6 for a while now to connect with an Azure database. For this database we use the ExecutionStrategy specific for Azure connection to have a more resilient connection:
public class MyDbConfiguration : DbConfiguration
{
public MyDbConfiguration()
{
SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
}
}
See the microsoft article about connection resilience for more info
Recently however we've added a connection to a different database which resides on a MSSQL database server where we want to use the default execution strategy. Since this runs in the same app domain, we run into a problem:
The default DbConfiguration instance was used by the Entity Framework before the 'MyDbConfiguration' type was discovered. An instance of 'MyDbConfiguration' must be set at application start before using any Entity Framework features or must be registered in the application's config file. See http://go.microsoft.com/fwlink/?LinkId=260883 for more information."
Reviewing the article linked in the error I see the following statement:
Create only one DbConfiguration class for your application. This class specifies app-domain wide settings.
I've tried solutions from several related questions to this, but I keep running in the same problem. The things I've tried basically come down to setting the custom DbConfiguration in different ways, through code, attribute or config file.
I think the solution is to set the execution strategy without a custom DbConfiguration, but I'm not really sure it is, and how I should do this.

Tests running simultaneously fail with "The instance of entity type ... cannot be tracked"

I'm running some unit tests on an asp.net core 2.0 project, using EF core and an in-memory database.
I'm creating the database like this:
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
var builder = new DbContextOptionsBuilder<TDatabaseContext>();
builder.UseInMemoryDatabase(Guid.NewGuid().ToString())
.UseInternalServiceProvider(serviceProvider)
.EnableSensitiveDataLogging();
var context = new MyDatabaseContext(builder.Options);
According to this post (How to isolate EF InMemory database per XUnit test), using a separate service provider and using a different name for every in-memory DB instance should ensure that each test has its own database which is not shared with other tests.
However, my tests run fine when I run them separately, but fail if I run them all together, with errors like this:
System.InvalidOperationException: The instance of entity type 'SomeModelObject' cannot be tracked because another instance with the key value 'Id:1' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
Is there anything else I should do to make sure that the DB context is not shared among different tests?

Using Azure Mobile Services Code First Generated Context class in another project

I have a Windows Azure Mobile Services app that has a Code First generated database. The connection string (for when run locally) looks like this:
<add name="MS_TableConnectionString" connectionString="Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\<database_name.mdf;Initial Catalog=<database_name>;Integrated Security=True;MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
I created a new Console App project, referencing the Mobile Services project, and copied this connection string to the App.config file
In Program.Main() I created a new instance of the Context class from the Designer in the Mobile Services project. But when I run the Console App, and try to access one of the DbSets made public by the Context, I get the following exception:
"An exception occurred while initializing the database. See the InnerException for details."
With an inner exception of:
"The underlying provider failed on Open."
Which in turn has an inner exception of:
"Cannot attach the file 'C:\\...\\<database_name>.mdf' as database '<database_name>'."
If I remove the AttachDbFilename part of the connection string in the Console App, I get the following exception at the same point in the code:
"Cannot create more than one clustered index on table 'dbo.<Table_Name>'. Drop the existing clustered index 'PK_dbo.<Table_Name>' before creating another"
Does anyone have any idea why it would be trying to create this new clustered index when there already appears to be one?
Or any ideas what connection string I should use just to get a normal read/write connection to the database without it doing anything weird? Is this related to database initialization?
Edit: I've had a bit more of a play around with this, this morning. I can get it working without exceptions if I remove the inheritance of "Microsoft.WindowsAzure.Mobile.Service.EntityData" from my model classes, so this appears to be pretty significant.
Ok I've just battled through this and got it working. Gonna type this up in case it helps anyone one day.
So the problem is something to do with the fact that my Code First model classes were inheriting from EntityData. As I said in my edit above, removing this inheritance does appear to fix the problem. I think this is because the EntityData class has both a property with a [Key] attribute and a separate property with a [Index(IsClustered = true)] attribute. Because you can't have more than one Clustered Index in a table, the database initialization fails. In the default Azure Mobile Services project, there must be some magic that means this doesn't happen somewhere - but from a separate project you get the "Cannot create more than one clustered index on table" exception.
So what I did was disable Database Initialization in the separate Console App by adding the line:
Database.SetInitializer<MobileServiceContext>(null);
...before the DbContext is instantiated.
This allows me to use the database initialized by the Mobile Services App as an existing database, without attempting to make any changes to it.
I also needed the following AppSetting in the config file of the Console App, in order for it to use the correct Schema Name:
<add key="MS_MobileServiceName" value="<CorrectSchemaName>" />

Entity Framework 5 : changing the database connection

I have a made an EntityFramework model based on my Demo Database.
I want to include connection strings for Staging and production and then in my Console App prompt the user to see which database they want to perform the operation on.
When I was prompted to setup the EF.edmx file I just chose to store the connection string in the app.config file. I have seen a link to change the Connection string of EntityFramework Context when initializing it here
However when I store another connection to my Staging Database, I get an error "Keyword not supported: 'metadata'"
So I stripped down the connection string not to include the EntityFramework type of parameters such as metadata=res://*/MyDBItems.csdl|res://*/MyDBItems.ssdl blah blah
and used a very simple database connection string
data source=myDB;initial catalog=myDB;user id=myUser;password=myPass;MultipleActiveResultSets=True;
now my Context does instanciate but when I get to a query I get another error about:
Code generated using the T4 templates for Database First and Model First development may not work correctly if used in Code First mode. To continue using Database First or Model First ensure that the Entity Framework connection string is specified in the config file of executing application. To use these classes, that were generated from Database First or Model First, with Code First add any additional configuration using attributes or the DbModelBuilder API and then remove the code that throws this exception.
How can I switch between different databases with Entity Framework 5, thanks!
forget it I got it....what I did...I made a 2nd constructor in the Context.cs file
public MyContext(string dbModel) : base("name="+dbModel) {
}
then in my app.config I have the various settings for the Demo,Staging & Production database connections....this took the full entityframework connection string. I think the link I provided was for Code-First but I am using Database First.

Categories