Entity Framework in child project not reading connectionstring sent to it - c#

I have created a separate Entity Framework project (with .NET 6) to be used with many different solutions. However, when I use it as a child project, it won't read the connection strings sent to it.
Here's what I mean: let's say the projects are called UserProject and EFProject.
EFProject is a class library which includes a DBContext and all the models representing the database.
It includes this class:
public partial class MyDataContext : DbContext
{
public MyDataContext()
{
}
public MyDataContext (DbContextOptions<MyDataContext> options)
: base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("test database connectionstring");
}
In my main project, UserProject, I have added this to appsettings.json:
"ConnectionStrings": {
"UseThisConnectionString": "production connectionstring"
}
and in Program.cs I have added:
builder.Services.AddDbContext<MyDataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("UseThisConnectionString"))
, ServiceLifetime.Singleton);
However, no matter what I try, the EFProject keeps using the test database connection string and not reading the connection string I send to it in the main project. It is like these two projects are not in talking terms about this.
How do I relay the proper connection string to the child project?

One approach is trying to check DbContextOptionsBuilder.IsConfigured property:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer("test database connectionstring");
}
Otherwise options will always be overridden (also possibly you can just remove the OnConfiguring override completely).
P.S.
In my personal experience I highly recommend against registering EF database context as ServiceLifetime.Singleton.

Related

Specifying correct context to add Identity into existing database

Similar questions have been posted before (1, 2, 3), but the situation is different.
I have an .Net Core 7 app using EFCore with some existing entities. Now that everything in there is tested and working, I'd like to add the Identity functionality and then link AspNetUsers to a couple of my existing entities.
Currently there are no identity tables, nor any sort of tables used by the Identity functionality.
Using the Microsoft tutorial Scaffold Identity into a Razor project without existing authorization I get to the step whereby I need to specify my database and user contexts.
After unsuccessfully looking for documentation I entered the database context as being the one which is used in my data project ('EventFinderData' below - a different project to my web app, but within the same solution). I then created a new user context...
After the scaffolding is complete, I get a number of ambiguous reference errors as the scaffolding process creates a new data context:
// This is a duplicate class of that within the EventFinderData project, which is already referenced within my web app
public class EventFinderContext : IdentityDbContext<EventFinderWebAppRazorUser>
{
public EventFinderContext(DbContextOptions<EventFinderContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
I tried to make both classes partial, but then when I add a migration I get:
More than one DbContext was found. Specify which one to use. Use the
'-Context' parameter for PowerShell commands and the '--context'
parameter for dotnet commands.
Some experts seem to recommend starting a new project with Identity included already, and building out from there, but its going to be a big job.
What do I need to modify please in order for this scaffolding process to bolt into my existing EFCore logic?
Alternatively is Identity supposed to use a dedicated (separate) context? That would seem unintuitive given the tables need relationships between identity entities and my existing entities.
Update
Based on suggestions, here are the three classes I get after adding the Identity functionality:
Data project (containing Entities and migrations):
EventFinderContext.cs
namespace EventFinderData
{
public class EventFinderContext : DbContext
{
public DbSet<EventItem> EventItems { get; set; }
public DbSet<EventCategory> EventCategories { get; set; }
public EventFinderContext(DbContextOptions<EventFinderContext> options) : base(options)
{
}
}
}
Web app project - Classes created by VS/Identity
Areas\Identity\Data\EventFinderWebAppRazorContext.cs
namespace EventFinderWebAppRazor.Data;
public class EventFinderWebAppRazorContext : IdentityDbContext<EventFinderWebAppRazorUser>
{
public EventFinderWebAppRazorContext(DbContextOptions<EventFinderWebAppRazorContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
Areas\Identity\Data\EventFinderWebAppRazorUser.cs
namespace EventFinderWebAppRazor.Areas.Identity.Data;
public class EventFinderWebAppRazorUser : IdentityUser
{
}
Program.cs
builder.Services.AddDbContextFactory<EventFinderContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("EventFinderConnString"),
ss => ss.UseNetTopologySuite())
.EnableSensitiveDataLogging());
builder.Services.AddDefaultIdentity<EventFinderWebAppRazorUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<EventFinderWebAppRazorContext>();
Not sure if something changed (.net6 here) but to add the dot net identity framework to your existing context all you have to do is just provide that context when adding it to the IServiceCollection.
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
})
.AddEntityFrameworkStores<ApplicationDbContext>()
You will need to change your DbContext so it inherits from
IdentityDbContext<ApplicationUser>
This will only provide the required services, not the different views and UX flows that comes when you scaffold it. So no login/logout, password resets, MFA setup etc...
I hope this will help.
Update
Based on the OP's update, the following updates are required.
Areas\Identity\Data\EventFinderWebAppRazorUser.cs
Move this class to your Data project. It will become a part of your own entity model. Make sure that the namespace is also updated. Although this is not really necessary, but it is nice to have all entities in the same namespace.
Areas\Identity\Data\EventFinderWebAppRazorContext.cs
Delete this file/class. This is the extra context created by the scaffolding process, but as we want to use our own context we don't need it.
EventFinderContext.cs
Update this class like below.
namespace EventFinderData
{
public class EventFinderContext : IdentityDbContext<EventFinderWebAppRazorUser>
{
public DbSet<EventItem> EventItems { get; set; }
public DbSet<EventCategory> EventCategories { get; set; }
public EventFinderContext(DbContextOptions<EventFinderContext> options) : base(options)
{
}
}
}
Program.cs
Update this file like below. And make sure all types resolve by updating the usings.
builder.Services.AddDbContextFactory<EventFinderContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("EventFinderConnString"),
ss => ss.UseNetTopologySuite())
.EnableSensitiveDataLogging());
builder.Services.AddDefaultIdentity<EventFinderWebAppRazorUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<EventFinderData.EventFinderContext>();

Why do we use DependecyInjection instead of OnConfiguring Method in the DbContext?

I have a class called StudentDbContext. I call theOnConfiguring method in it. I saw that dependency injection is used in some training videos. Here we already use context once. Why should I use dependency injection instead of OnConfiguring?
Option-1
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySQL("...");
}
Option-2
public StudentDbContext(DbContextOptions<StudentDbContext> context) : base(context)
{
}
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySQL("...");
}
With this approach, you decide exactly which provider (MySQL) and which connection-string to use within StudentDbContext directly. If you want to change this to use a different provider or connection-string, you'll have to modify the StudentDbContext class directly. For example, consider a situation where you want to use a different provider for Development vs Production.
public StudentDbContext(DbContextOptions<StudentDbContext> context)
: base(context) { }
This approach allows you to configure both the provider and the connection-string from outside of the StudentDbContext.
Using Dependency Injection, you'll see something like this in Startup.ConfigureServices (or Program.cs with ASP .NET Core 6):
services.AddDbContext<StudentDbContext>(options =>
{
options.UseMySQL("...");
});
This second approach becomes more powerful when you want to use different providers/connection-strings (or even other settings) based on an environment, for example:
if (hostEnvironment.IsDevelopment())
{
services.AddDbContext<StudentDbContext>(options =>
{
options.UseMySQL("...");
});
}
else
{
services.AddDbContext<StudentDbContext>(options =>
{
options.UseSqlServer("...");
});
}
With dependency injection the DbContext doesn't need to know about the actual database in use. Furthermore, the DbContext doesn't even need to know which configuration settings should be used for establishing database connections.
This makes your code more independent. This e.g. makes your code more testable. There are other advantages, too.
you can also configure both approaches for use
db context
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseMySQL("...");
}
}
or startup configuration
services.AddDbContext<StudentDbContext>(options =>
{
options.UseMySQL("...");
});
in this case if you don't configure the startup configuration or if you are using you db context in another project without dependency injection will be uses a local db contex configuration, otherwise global.

EF Core 3rd party library migration not running

I have created a 3rd party library that has a db table. The purpose of this library is to be used across several other projects I have. It provides infrastructure work (specific user permissions). So in order not to write the same code in each service I have refactored it into a library. This libpublic class
public class UserAccessContext : DbContext
{
public UserAccessContext(DbContextOptions options)
: base(options)
{
Database.Migrate();
}
public DbSet<ClientPermission> ClientPermission{ get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ClientPermission>().HasKey(c => new { c.UserId, c.PermissionId});
}
}
I have created migration scripts (Add-Migration...) and the script seems ok. It creates the ClientPermission table. Imported the library to one of the services (Service A) and I can see that the migration script of UserAccess library does not run when I start Service A. The only migration script that runs is the one that belongs to ServiceA. When UserAccessContext is called Database.Migrate() is executed but it does not find any changes that need to be applied.
Why does this happen and how can I enforce it to run UserAccessContext migration script?
Make sure the right MigrationsAssembly is used in your DbContextOptions. By default, the current assembly is used but you need to use the assembly where the migration code is (your library here if I understand you well). Check this link :
https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/projects?tabs=dotnet-core-cli

Intercepting SqlServerMigrationsSqlGenerator to prevent migration of certain tables?

I am trying to use EF Core 3.0 migrations with a hybrid of an existing tables and new tables built with code first. To prevent the scaffolding of the existing tables I would like to decorate the model class with an attribute (fluently or annotations) so that the migration code generation for those tables is skipped but the model is still built into the DbContext class.
The approach I'm taking is to add the following lines to OnConfiguring
optionsBuilder.ReplaceService<IMigrationsSqlGenerator, SkipMigrator>();
And then creating a SkipMigrator with the following code
public class SkipMigrator:SqlServerMigrationsSqlGenerator
{
public SkipMigrator(
MigrationsSqlGeneratorDependencies dependencies,
IMigrationsAnnotationProvider migrationsAnnotations)
: base(dependencies, migrationsAnnotations){}
protected override void Generate(
MigrationOperation operation,
IModel model,
MigrationCommandListBuilder builder)
{
if (operation.FindAnnotation("SkipMigrations")!=null)
{
Console.WriteLine("Skipping table:");
}
else
{
base.Generate(operation,model,builder);
}
}
}
I assumed that the Generate method was what triggered the creation of the migration code file but it never gets called. Id there a different place I should be intercepting the code generation?
If there a different/simpler way to tell migrations to skip tables yet still keep them in my DbContext?
Your means of trying to create your own IMigrationsSqlGenerator was correct, and I've used that approach before to alter the SQL that is generated as part of a migration.
services.AddDbContext<MyDbContext>(opt =>
{
opt.UseSqlServer();
opt.ReplaceService<IMigrationsSqlGenerator, SkipMigrator>();
});
However, as of EF Core 5.0 it's now much easier to exclude specific tables from migrations using the ExcludeFromMigrations() method on a TableBuilder:
public class ReportingContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().ToTable(nameof(Users), t => t.ExcludeFromMigrations());
}
}
More info: https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-efcore-5-0-rc1/#exclude-tables-from-migrations
If there a different/simpler way to tell migrations to skip tables yet still keep them in my DbContext?
Yes, but it requires a different approach.
Instead of using your current DbContext class for creating migrations, create a SecondDbContext class only for the very purpose of creating migrations. This new SecondDbContext class will hold the DbSets<T> that you want EF to do its migrations on.
Then simply specify the second context when calling add-migration UpdateTable -c SecondDbContext and then update-database -c SecondDbContext.

Partial EF Context OnModelCreating Using Scaffold-DbContext

I'm using doing database first development and using Scaffold-DbContext to create my entity models project. In that project, I have a partial dbContext where I override methods like SaveChangesAsync to set certain properties like, "LastModifiedBy".
I'm looking to soft-delete records using dateDeleted/userDeleted columns. When I go to override the OnModelCreating routine, I see that it's already defined on the auto-generated partial context.
I'm trying to do something like the following:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SomeEntity>().HasQueryFilter(x => x.DateDeleted == null);
}
Does anyone have any suggestion as to how I can do this in a partial class way that still allows me to regenerate the original context on the fly using Scaffold-DbContext?
Also note, I'm using .NET Core 2.1.5
ANSWER FROM David Browne - Microsoft
Add a static property on the partial context
public static bool GlobalFiltersAdded { get; set; } = false;
Then add a routine to add your filters:
private void AddGlobalFilters(ModelBuilder modelBuilder){
SomeContext.GlobalFiltersAdded = true;
}
Then in your generated context add the following:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
AddGlobalFilters(modelBuilder);
//OTHER LOGIC WILL BE HERE
}
Options:
1) Delete the generated OnModelCreating after re-scaffolding. It's already a manual process, and generates a compile error if you forget.
2) Use a DbContext inherited from the generated DbContext.
3) Use a 3rd party tool or library like: EF Core Power Tools

Categories