Modifications and Extensions of the DBContext class to accommodate EF Global Query Filters in a DB first project
If DBContext class is a new deal for you, check this out to learn more about it : https://docs.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext
Being in a DB first approach comes with some challenges. I am sure that you are aware that every time you are doing a scaffold, the class that extends DBContext gets fully regenerated. That begin said, we had to come up with a solution that has minimal impact on those methods.
There were 2 changes that we did in order to make this happen.
1. Modify OnConfiguring() method:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { OnConfiguringFinal(optionsBuilder); }
Why OnConfiguring() ? Because, and this is pretty important, this is the only method in the DBContext extended class that is called every time the DBContext is instantiated, and we will need that for the EF Core Global Query Filters.
2. Modify OnModelCreating() method:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // all you modelBuilder Entities ... // a method that calls the class but in another .cs file so it is not overwritten by the scaffold OnModelCreatingFinal(modelBuilder); }
Why OnConfiguring() ? Because, after all the modelBuilder entities are instantiated we immediately apply the filters on top of them.
And this all you have to do every time you scaffold the app.
Moving forward, we have created another class so we can base jump to the Global Query Filters. Also, we are using this class to slightly override the SaveChangesAsync() method that we will use in the controllers that are inserting/updating data ( like a POST, PATCH or a PUT method ).
public partial class MasterContext : DbContext { private readonly IHttpContextAccessor _httpContextAccessor; public MasterContext(DbContextOptions<MasterContext> options, IHttpContextAccessor httpContextAccessor) : base(options) { _httpContextAccessor = httpContextAccessor; } protected void OnModelCreatingFinal(ModelBuilder modelBuilder) { // this where the default OnModelCreating() jumps OnModelCreatingFilters(modelBuilder); } partial void OnModelCreatingFilters(ModelBuilder modelBuilder); protected void OnConfiguringFinal(DbContextOptionsBuilder optionsBuilder) { ... // keep in mind this line because we will approach it chapter 4 Implementation of //ModelCacheKey in EF Core optionsBuilder.UseSqlServer(connectionString).ReplaceService<IModelCacheKeyFactory, MasterModelCacheKeyFactory>(); } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { #region Automatically set "createdOn" property on an a new entity var CreatedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Added).ToList(); var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Name).Value; var userTenantClaims = _httpContextAccessor.HttpContext.User.Claims.Where(c => c.Type == "tenant_ids").ToList(); CreatedEntities.ForEach(E => { bool tenantallow = false; var tenantprop = E.Metadata.FindProperty("TenantId"); if(tenantprop!=null) { foreach (var userTenantClaim in userTenantClaims) { if(userTenantClaim.Value.ToString() == E.Property("TenantId").CurrentValue.ToString()) { tenantallow = true; } } if (tenantallow == false) { throw new ArgumentException( $"You do not have permissions on {E.Property("TenantId").CurrentValue.ToString()} tenant."); } } var prop = E.Metadata.FindProperty("CreatedOn"); if (prop != null) { try { E.Property("CreatedOn").CurrentValue = DateTimeOffset.UtcNow; E.Property("CreatedBy").CurrentValue = userId; E.Property("ModifiedOn").CurrentValue = DateTimeOffset.UtcNow; E.Property("ModifiedBy").CurrentValue = userId; } catch (Exception e) { throw e; } } }); #endregion #region Automatically set "modifiedOn" property on an a new entity var ModifiedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Modified).ToList(); ModifiedEntities.ForEach(E => { var prop = E.Metadata.FindProperty("ModifiedOn"); if (prop != null) { try { E.Property("ModifiedOn").CurrentValue = DateTimeOffset.UtcNow; E.Property("ModifiedBy").CurrentValue = userId; } catch (Exception e) { throw; } } }); #endregion return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } }
This sums up the modification needed so you can start using ModelCacheKey and Global Query Filters in .NET EF Core.
Chapter 4 : Implementation of ModelCacheKey in EF Core