Specifying correct context to add Identity into existing database - c#

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>();

Related

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.

Error defining DbContext of type IdentityDbContext

We have an Azure Functions app that uses three databases, one of which is of type "IdentityDbContext".
Out of the blue, after months of happy use of these contexts, after a rebuild, we run into this error when firing up the Function app:
Method Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContext: type argument 'xxx.ApplicationDbContext' violates the constraint of type parameter 'TContext'.
We've tried to find a change that might cause this, but we are stumped…
Similar questions in Stackoverflow etc suggest it was an issue in earlier versions of Azure Functions, but should be resolved by now.
This is the class for the offending DBcontext:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
Database.EnsureCreated();
}
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);
builder.Entity<ApplicationUser>().HasMany(e => e.Claims).WithOne().HasForeignKey(e => e.UserId).IsRequired().OnDelete(DeleteBehavior.Cascade);
}
}
And this is where all hell breaks loose:
var services = new ServiceCollection()
.AddDbContext<MobidotContext>(options => options.UseSqlServer(config.GetConnectionString("MobidotDbConnection")))
.AddDbContext<MobilityDbContext>(options => options.UseSqlServer(config.GetConnectionString("MobilityDbConnection")))
.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(config.GetConnectionString("DefaultConnection")));
Well, it appeared that an upgrade to Entity framework caused havoc with the IdentityDBcontext. Downgraded back to 2.2.2, and we're back in business.
I have a feeling we're back in dll hell from ten years ago… :-(

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

Changing the default SQL Server schema used from [dbo] to something else

I am using EF5 with the DefaultMembershipProvider and want to control the schema used in SQL Server for the tables created by the DefaultMembershipProvider.
EF5 says this is doable as:
modelBuidler.Entity<MyEntity>().ToTable("MyTable", "MySchema");
but as these are not "my" entities I cannot do it this way.
Questions:
1) So how do I do this in EF5?
2) Is this dealt with in EF6 when using DbModelBuilder.HasDefaultSchema?
Yes indeed I would upgrade to EF6 and then make your context look like this, e.g.:
public partial class BlogContext : DbContext
{
public BlogContext()
: base("BlogDb")
{
Database.SetInitializer<BlogContext>(null);
}
public DbSet<BlogPost> BlogPosts { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingEntitySetNameConvention>();
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.HasDefaultSchema("");
}
}
You see setting the default schema at the end to "", which also works, in my case, Oracle 12c takes the login user name as schema and everything works fine. Of course you could also load the schema name from configuration and insert it there.
Note: Automatic migrations will NOT work anymore, as this seems to confuse the system quite much. See here for a possible solution so that at least explicit migrations will work somehow: Entity Framework using IdentityDbContext with Code First Automatic Migrations table location and schema?

Categories