I am trying to rekindle this topic. It was once started over 10 years ago, and while there were some comments that are a couple years ago, it has now been closed. Yet again, this still is kind of unclear to me.
A lot of people put their opinions on it, and whilist there were some good reasons and opinions, the practical approach, and overall conclusion is still unclear, at least to me.
My situation: I am trying to create a way more complicated than neccesary app to research and test things. I've been using only one DbContext up until now and now I wish to separate it by creating a new one for identity/security (I am aware that IdentityDbContext exists, and while this is a smarter solution, I wanna play around).
As I managed to configure the second DbContext and create a migration, the migration state has been resetted to "Initial" and new tables are created (instead of using the old ones).
My question: What is a good practical example where introduction of the new DbContext is applied and how to replicate the up until now Migration Snapshot to it (in order to continue the sequence). Also, to replicate the future state of Migration Snapshot to the original DbContext and so on.
Here are some code examples that I managed to scribble:
This is a part of the base class extension for DbContexts.
public class DbContextExtend : DbContext
{
protected readonly ICurrentUserService _userService;
protected readonly IDateTime _dateTime;
public DbContextExtend(DbContextOptions<ReservationDbContext> options) : base(options) { }
public DbContextExtend(DbContextOptions<ReservationDbContext> options,
IDateTime datetime,
ICurrentUserService userService) : base(options)
{
_dateTime = datetime;
_userService = userService;
}
public DbContextExtend(DbContextOptions<SecurityDbContext> options) : base(options) { }
public DbContextExtend(DbContextOptions<SecurityDbContext> options,
IDateTime datetime,
ICurrentUserService userService) : base(options)
{
_dateTime = datetime;
_userService = userService;
}
public DbSet<Audit> Audits { get; set; }
public async override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
//var auditEntries = await OnBeforeSaveChangesAsync();
var result = await base.SaveChangesAsync(cancellationToken);
//await OnAfterSaveChanges(auditEntries);
return result;
}
}
A freshly introduced DbContext - OnModelCreating, Ignore Reservations and everything after it in order to focus on the 3 tables of importance (is there a better way to do this).
public class SecurityDbContext : DbContextExtend, ISecurityDbContext
{
public SecurityDbContext(DbContextOptions<SecurityDbContext> options) : base(options) { }
public SecurityDbContext(DbContextOptions<SecurityDbContext> options,
IDateTime datetime,
ICurrentUserService userService) : base(options, datetime, userService) { }
public DbSet<User> Users { get; set; }
public DbSet<LoginDetails> LoginDetails { get; set; }
public DbSet<Role> Roles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new Configurations.SecurityConfiguration.RoleConfiguration());
modelBuilder.ApplyConfiguration(new Configurations.SecurityConfiguration.UserConfiguration());
modelBuilder.ApplyConfiguration(new Configurations.SecurityConfiguration.LoginDetailsConfiguration());
modelBuilder.Ignore<Reservation>();
}
}
The DbContext that was in use up untill now. Ignoring the table that should be out of it, the LoginDetails.
public class ReservationDbContext : DbContextExtend, IReservationDbContext
{
public ReservationDbContext(DbContextOptions<ReservationDbContext> options) : base(options) { }
public ReservationDbContext(DbContextOptions<ReservationDbContext> options,
IDateTime datetime,
ICurrentUserService userService) : base(options, datetime, userService) { }
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<LoginDetails> LoginDetails { get; set; }
public DbSet<EventType> EventTypes { get; set; }
public DbSet<Event> Events { get; set; }
public DbSet<Question> Questions { get; set; }
public DbSet<EventQuestion> EventQuestions { get; set; }
public DbSet<EventOccurrence> EventOccurrences { get; set; }
public DbSet<Ticket> Tickets { get; set; }
public DbSet<Reservation> Reservations { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new EventTypeConfiguration());
modelBuilder.ApplyConfiguration(new EventConfiguration());
modelBuilder.ApplyConfiguration(new QuestionConfiguration());
modelBuilder.ApplyConfiguration(new EventOccuranceConfiguration());
modelBuilder.ApplyConfiguration(new ReservationConfiguration());
modelBuilder.ApplyConfiguration(new TicketConfiguration());
modelBuilder.ApplyConfiguration(new UserConfiguration());
modelBuilder.ApplyConfiguration(new RoleConfiguration());
modelBuilder.Ignore<LoginDetails>();
}
}
UserConfiguration - an example of a configuration and it is a table that separates both contexts (but is contained in both).
public class UserConfiguration : AuditableEntityConfiguration<User>
{
public override void ConfigureAuditableEntity(EntityTypeBuilder<User> builder)
{
builder.HasKey(u => u.Id);
builder.Property(u => u.Email)
.HasMaxLength(128)
.IsRequired();
builder.Property(u => u.Name)
.IsRequired();
builder.Property(u => u.PhoneNumber)
.HasMaxLength(20)
.IsRequired(false);;
builder.Property(u => u.RoleId)
.IsRequired();
builder.HasOne(u => u.Role)
.WithMany(r => r.Users)
.HasForeignKey(u => u.RoleId);
builder.HasOne(u => u.LoginDetails)
.WithOne(ld => ld.User)
.HasForeignKey<LoginDetails>(u => u.UserId)
.IsRequired();
}
}
It might be worth noting that I also decided to separated SecurityDbContext logic to a different project.
Please feel free to give me all advice and real world experience that you can. I would greatly appreciate it!
You should generate different migration for each DbContext. If you have different entities that you want to be in the same db table, explicitly say that in each configuration, using the ToTable() method. Also, their configuration should match. Finally, you should also be explicit about the db name, so in each OnModelCreating you should pass in the builder.HasDefaultScheme() the same value.
Let's say i have an IdentityDbContext that inherits from the IdentityDbContext<>, the default that you get in asp, but with an AppUser that inherits from the IdentityUser, so i have a configuration for that. Then i would have something like that:
public class IdentityDbContext : IdentityDbContext<AppUser>
{
//Other stuff
protected override void OnModelCreating(ModelBuilder builder)
{
builder.HasDefaultSchema("TestDb");
builder.ApplyConfiguration(new AppUserConfiguration())
base.OnModelCreating(builder);
}
}
Then i have a WriteDbContext that inherits from the DbContext, and i have a configuration for a customer:
public class WriteDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
//Other stuff
protected override void OnModelCreating(ModelBuilder builder)
{
builder.HasDefaultSchema("TestDb");
builder.ApplyConfiguration(new CustomerConfiguration())
base.OnModelCreating(builder);
}
}
At that point i need to generate a migration for each DbContext and then apply them. Then i would have all the identity stuff and the customer in the same Database.
I could also have a CustomerReadModel that i can use only for reads, so it does not have any logic, private fields and maybe has navigation to other ReadModels. As long as they all have the same configuration, for example the FirstName in both of them is configured to be nvarchar(50), if the customer has one Address (as an entity), then the CustomerReadModel has also one or it has a AddressReadModel configured as the Address etc, and in both configuration i have builder.ToTable("Customers") the will both point to the same customers db table:
public class ReadDbContext: DbContext
{
public DbSet<CustomerReadModel> Customers{ get; set; }
//Other stuff
protected override void OnModelCreating(ModelBuilder builder)
{
builder.HasDefaultSchema("TestDb");
builder.ApplyConfiguration(new CustomerReadModelConfiguration())
base.OnModelCreating(builder);
}
}
And i now should not add a migration for the ReadDbContext, since the database is already configured.
You can also check these out if you want (disclaimer: they are mine):
https://www.youtube.com/watch?v=QkHfBNPvLms
https://www.youtube.com/watch?v=MNtXz4WvclQ
Edit : I made a demo some time ago that has some of the above: https://github.com/spyroskatsios/MakeYourBusinessGreen
Related
I'm new to ASP.NET Core and i'm trying to insert an entity into an Entity Framework Core model scaffolded from a simple existing MariaDB database.
This is the entity model:
public class ScrapeAsincroni
{
public int Id { get; set; }
public string Paese { get; set; }
public string Engine { get; set; }
public string Keywords { get; set; }
}
This is the controller action that is supposed to add the entity:
public JsonResult create(string paese, string engine, string keywords)
{
ScrapeAsincroni scrapeAsincrono = new ScrapeAsincroni {
Paese = paese,
Engine = engine,
Keywords = keywords
};
_context.Add(scrapeAsincrono);
try
{
_context.SaveChangesAsync();
}
catch (Exception ex)
{
return Json(new Dictionary<string, int?> { { "id", null } });
}
return Json(new Dictionary<string, int?>{ {"id", scrapeAsincrono.Id} });
}
The database context (_context) has been initialized on the controller's constructor.
the line
_context.Add(scrapeAsincrono);
throws the following exception:
System.InvalidOperationException: The entity type 'ScrapeAsincroni' was not found. Ensure that the entity type has been added to the model.
This is the modelBuilder code relative to this model
public partial class ScraperDbContext : DbContext
{
public ScraperDbContext()
{
}
public ScraperDbContext(DbContextOptions<ScraperDbContext> options)
: base(options)
{
}
public virtual DbSet<ScrapeAsincroni> ScrapeAsincroni { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
optionsBuilder.UseMySql("server=#.#.#.#;port=####;user=####;password=####;database=####", x => x.ServerVersion("10.3.25-mariadb"));
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ScrapeAsincroni>(entity =>
{
entity.HasComment("Informazioni su una ricerca asincrona dello Scraper");
entity.Property(e => e.Id)
.HasColumnName("id")
.HasColumnType("int(11)");
entity.Property(e => e.Engine)
.HasColumnName("engine")
.HasColumnType("varchar(255)")
.HasCharSet("utf8")
.HasCollation("utf8_general_ci");
entity.Property(e => e.Keywords)
.IsRequired()
.HasColumnName("keywords")
.HasColumnType("text")
.HasCharSet("utf8")
.HasCollation("utf8_general_ci");
entity.Property(e => e.Paese)
.HasColumnName("paese")
.HasColumnType("varchar(255)")
.HasCharSet("utf8")
.HasCollation("utf8_general_ci");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
I can't seem to understand why this happens, and couldn't find any solutions online.
add
public DbSet<ScrapeAsincroni> ScrapeAsincronis { get; set; }
in your context file for adding an entity to your model.
It was, of course, a trivial error that i would fix the next day. I was using the base DbContext class, while i should have used the custom database context class which i defined when scaffolding the database.
This is how the context was instantiated before in the controller script:
private readonly DbContext _context;
public ScrapeAsincroniController(DbContext context)
{
_context = context;
}
now it is:
private readonly ScraperDbContext _context;
public ScrapeAsincroniController(ScraperDbContext context)
{
_context = context;
}
I also changed this line in startup.cs:
services.AddDbContext<ScraperDbContext>(options => options
.UseMySql("<your_connection_string>",
mysqlOptions => mysqlOptions.ServerVersion(new Pomelo.EntityFrameworkCore.MySql.Storage.ServerVersion(new Version(10, 3, 25), ServerType.MariaDb))
)
);
I am still relatively new to EF Core and beforehand I used PetaPoco, so please forgive my ignorance. In my database, I added the following fields to my AspNetUsers table:
Elevated
Deactivated
FirstName
LastName
I then created the following classes following this blog article:
public partial class ApplicationUser : IdentityUser
{
public bool? Deactivated { get; set; }
public bool? Elevated { get; set; }
[StringLength(255)]
public string FirstName { get; set; }
[StringLength(255)]
public string LastName { get; set; }
}
and
public class ApplicationClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public ApplicationClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{ }
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
if (!string.IsNullOrWhiteSpace(user.FirstName))
{
((ClaimsIdentity)principal.Identity).AddClaims(new[] {
new Claim(ClaimTypes.GivenName, user.FirstName)
});
}
if (!string.IsNullOrWhiteSpace(user.LastName))
{
((ClaimsIdentity)principal.Identity).AddClaims(new[] {
new Claim(ClaimTypes.Surname, user.LastName)
});
}
return principal;
}
}
I also setup the AddIdentity and AddScoped methods too in my Startup.cs file.
What I do not understand going forward from this point is how to query the table, returning my custom properties. In my controller, I want to do something like this:
/// <summary>
/// Gets every User.
/// </summary>
/// <returns>HTTP Result</returns>
[HttpGet]
[Route("")]
public async Task<ActionResult<IEnumerable<ApplicationUser>>> GetUsers()
{
var users = await this._context.Users.Select(user => new
{
user.Id,
user.Decativated,
user.Elevated,
user.Email,
user.FirstName,
user.LastName
}).ToListAsync();
return Ok(users);
}
But obviously I can't because the properties don't exist on the Users DbSet.
So long story short, I think that I have everything setup properly but how do I actually query my extended IdentityUser?
EDIT
As requested, the following is my DbContext. I only have a dummy table wired up right now as I was just trying to get the extensions on the AspNetUsers table working first:
public partial class [removed for confidentiality]Context : IdentityDbContext
{
public [removed for confidentiality]Context()
{
}
public [removed for confidentiality]Context(DbContextOptions<[removed for confidentiality]Context> options)
: base(options)
{
}
public virtual DbSet<Foo> Foos { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "[removed for confidentiality]");
modelBuilder.Entity<Foo>(entity =>
{
entity.Property(e => e.FooName).IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
You have to specify type parameter(s) for IdentityDbContext. You need to inherit from IdentityDbContext<TUser> class. Without type parameters you are using classes defined in ASP.NET Core Identity. Everything you'd like to change must be reflected in your code.
public partial class [removed for confidentiality]Context : IdentityDbContext<ApplicationUser>
{
public [removed for confidentiality]Context()
{
}
public [removed for confidentiality]Context(DbContextOptions<[removed for confidentiality]Context> options)
: base(options)
{
}
public virtual DbSet<Foo> Foos { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "[removed for confidentiality]");
modelBuilder.Entity<Foo>(entity =>
{
entity.Property(e => e.FooName).IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
i had an application where data is saved in different sql schema for different Users.
For e.g.
User 1 Data is saved in SCHEMA1
User 2 Data is saved in SCHEMA2
Previously application was developed in MVC 3 and it is working fine and as expected.
Now we are migrating application in .Net Core 2.2 in which this fucntionality is not working
.net core does not have IDbModelCacheKeyProvider due to this only one schema is working
Below is the DBContext File
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
//public string Schema { get; set; }
private readonly IConfiguration configuration;
public string SchemaName { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public ApplicationDbContext(string schemaname)
: base()
{
SchemaName = schemaname;
}
public DbSet<EmployeeDetail> EmployeeDetail { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configuration = builder.Build();
optionsBuilder.UseSqlServer(configuration["ConnectionStrings:SchemaDBConnection"]);
var serviceProvider = new ServiceCollection().AddEntityFrameworkSqlServer()
.AddTransient<IModelCustomizer, SchemaContextCustomize>()
.BuildServiceProvider();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.RemovePluralizingTableNameConvention();
modelBuilder.HasDefaultSchema(SchemaName);
base.OnModelCreating(modelBuilder);
}
public string CacheKey
{
get { return SchemaName; }
}
}
public class SchemaContextCustomize : ModelCustomizer
{
public SchemaContextCustomize(ModelCustomizerDependencies dependencies)
: base(dependencies)
{
}
public override void Customize(ModelBuilder modelBuilder, DbContext dbContext)
{
base.Customize(modelBuilder, dbContext);
string schemaName = (dbContext as ApplicationDbContext).SchemaName;
(dbContext as ApplicationDbContext).SchemaName = schemaName;
}
}
My question is how to change schemaName at runtime
So what is the right way to organize that mechanism:
Figure out the schema name by the user credentials;
Get user-specific data from database from specific schema.
I was able to change schema at runtime by changing onConfiguring Method
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public string SchemaName { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public ApplicationDbContext(string schemaname)
: base()
{
SchemaName = schemaname;
}
public DbSet<EmployeeDetail> EmployeeDetail { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configuration = builder.Build();
var serviceProvider = new ServiceCollection().AddEntityFrameworkSqlServer()
.AddSingleton<IModelCustomizer, SchemaContextCustomize>()
.BuildServiceProvider();
optionsBuilder.UseSqlServer(configuration["ConnectionStrings:SchemaDBConnection"]).UseInternalServiceProvider(serviceProvider);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// modelBuilder.MapProduct(SchemaName);
modelBuilder.RemovePluralizingTableNameConvention();
if (!string.IsNullOrEmpty(SchemaName))
{
modelBuilder.HasDefaultSchema(SchemaName);
}
base.OnModelCreating(modelBuilder);
}
public string CacheKey
{
get { return SchemaName; }
}
public class SchemaContextCustomize : ModelCustomizer
{
public SchemaContextCustomize(ModelCustomizerDependencies dependencies)
: base(dependencies)
{
}
public override void Customize(ModelBuilder modelBuilder, DbContext dbContext)
{
base.Customize(modelBuilder, dbContext);
string schemaName = (dbContext as ApplicationDbContext).SchemaName;
(dbContext as ApplicationDbContext).SchemaName = schemaName;
}
}
}
The best way is to use Multi-Tenancy Architecture to be able to use database schema per user ( Tenant )
This architecture is recommended for Saas appllications
Concepts
Let’s agree on some basic concepts:
We use a tenant identification (or resolution) strategy to find out what tenant are we talking to
A tenant DbContext access strategy will figure out the way to retrieve (and store)
This article will show you how to implement Multi-tenancy app using .net core : https://stackify.com/writing-multitenant-asp-net-core-applications/
I'm trying to publish a code-first webapp that uses sqlite to Azure. Now, the application is working fine locally. but when I publish, it seems that the database file is not being created properly, and I get the following error:
SqliteException: SQLite Error 1: 'no such table: Blogs'.
Microsoft.Data.Sqlite.Interop.MarshalEx.ThrowExceptionForRC(int rc, Sqlite3Handle db)
I am not sure as to why the Blogs table isn't created (and I assume the rest aren't created either).
my ApplicationDbContext.cs looks like this:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public ApplicationDbContext()
{
Database.EnsureCreated();
Database.Migrate();
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
public DbSet<Publication> Publications { get; set; }
public DbSet<Blog> Blogs { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<ApplicationUser> ApplicationUser { get; set; }
}
Configuration:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))
);
}
and DefaultConnection in appsettings.json:
"ConnectionStrings": {
"DefaultConnection": "Filename=.\\mydb.sqlite"
},
Furthermore, when I download mydb.sqlite and open it, its completely empty.
#Tseng your instinct about EnsureCreated was right. I ended up solving this by clearing the constructor and adding this to the end of the Configure method in Startup.cs
var serviceScopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
using (var serviceScope = serviceScopeFactory.CreateScope())
{
var dbContext = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
dbContext.Database.EnsureCreated();
}
I found the answer here
I have a DbContext like this,
public class EPDContext : TrackerContext
{
public EPDContext()
: base("name=DevelopmentApplicationServices")
{
Database.SetInitializer<EPDContext>(new EPDDBInitializer());
this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
}
public DbSet<TaskRevision> TaskRevisions { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<TaskRevision>().HasMany(x => x.Models).WithMany().Map(x => x.MapLeftKey("TaskRevisionID").MapRightKey("ModelId").ToTable("TaskRevision2Models"));
}
}
public class EPDDBInitializer : CreateDatabaseIfNotExists<EPDContext>
{
protected override void Seed(EPDContext context)
{
//// My Seeding data goes here
base.Seed(context);
}
}
And my Entity:
[TrackChanges]
public class TaskRevision
{
#region properties
[Key]
public Guid TaskRevisionID { get; set; }
public virtual List<Model> Models { get; set; }
}
and my migration configuration class looks like:
internal sealed class Configuration : DbMigrationsConfiguration<PW.EPD.Data.EPDContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
protected override void Seed(PW.EPD.Data.EPDContext context)
{
}
}
I got this error "There is already an object named 'TaskRevisions' in the database entity framework." when I execute my application. DB has created successfully and there is no seeding data.
At the same time when I execute the same code after removing the onModelCreating() override method, db has created with seed data.
What I did wrong here, kindly correct me.
Thanks in advance