2 min read

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