1

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!

  • _"new tables are created (instead of using the old ones)."_ - Scaffold the second context using only needed tables. Delete db sets from the old one, add the migration and manually clean it (if needed). – Guru Stron Feb 05 '23 at 01:34

2 Answers2

4

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 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

Edit 2: After David's suggestion, I add the code for registering the DbContexts. In this example I use Sqlserver:

  private static IServiceCollection ConfigureSqlServer(this IServiceCollection services, IConfiguration configuration)
  {
    services.AddDbContext<WriteDbContext>(options =>
    {
        options.UseSqlServer(configuration.GetConnectionString("SqlDb"));
    });

    services.AddDbContext<ReadDbContext>(options =>
    {
        options.UseSqlServer(configuration.GetConnectionString("SqlDb"))
            .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); // Disable tracking since it's only for read purposes
    });

    services.AddDbContext<IdentityDbContext>(options =>
    {
        options.UseSqlServer(configuration.GetConnectionString("SqlDb"));
    });

    return services;
  }
spyros__
  • 1,311
  • 6
  • 8
  • 1
    I am sorry for this late of response. I'll probably be further separating the ReservationsDbContext to Read and Write versions (at least to try it out). Thanks for your input. – Pavle Ćurčić Mar 01 '23 at 16:04
  • 1
    This is great but I suggest adding to your answer the DI calls to put these classes in builder.Services. And if you do, make sure you set change tracking off in the Read context. TIA – David Thielen Mar 25 '23 at 19:24
1

What I eventually did was create another SecurityDbContext that inherits from the same DbContextExtend base. Also, ReservationsDbContext is inherited from the same base.

All the migrations are going through ReservationsDbContext, while database activities are going through their respective context.

At the end it wasn't that necessary to replicate migrations from one DbContext to another.