CQRS db Contexts with .NET Core
We're developing a new app using .NET Core 2.2 and it's time for some redesign. When it came to EF db Contexts we wanted to separate our Reads from our Writes (a la CQRS) more explicitly. We needed to work with the same entities in both BUT we still wanted to optimize performance with Reads and customize Writes separately so below is a quick and easy implementation.
In the Write Context, you can add any custom logic that will only be applicable to write actions such as create, update, and delete e.g. setting the Created Date field globally if you have standard audit fields on your db tables.
Base Context
Setup a Base Context class that you will inherit from; you can either inherit from DbContext or in this case IdentityDbContext since we're also leveraging Identity Core.public abstract class BaseContext<TContext> : IdentityDbContext<User> where TContext : DbContext { protected BaseContext(DbContextOptions<TContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } // TODO add DbSet<> for each entity/table (as needed) public DbSet<Audit> Audit { get; set; } }
Read and Write Contexts
Setup both a Read and a Write Context that inherit from the Base Context above. In the Read Context you override SaveChangesAsync to avoid any write actions so it's dedicated for read actions only.public class ReadContext : BaseContext<ReadContext> { public ReadContext(DbContextOptions<ReadContext> options) : base(options) { } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { throw new InvalidOperationException("This context is read-only."); } }
In the Write Context, you can add any custom logic that will only be applicable to write actions such as create, update, and delete e.g. setting the Created Date field globally if you have standard audit fields on your db tables.
public class WriteContext : BaseContext<WriteContext> { public WriteContext(DbContextOptions<WriteContext> options) : base(options) { } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { // TODO add custom logic return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } }
Startup
In Startup.cs, you then setup both contexts and configure each of them separately, for example you can globally enforce NoTracking for the Read Context to improve performance.public void ConfigureServices(IServiceCollection services) { ... var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<ReadContext>(options => { options.UseSqlServer(connectionString); options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); }); services.AddDbContext<WriteContext>(options => options.UseSqlServer(connectionString)); ... }
Comments
Post a Comment