The Story of My Life

Diary of a noob programmer

Entity Framework Core And Global Filters

There are times in EF Core which you may want to put a global filter on all your models, or some of them. And to do that, reflection is our weapon through the scary path.

In my case, I had several entity of which i didn’t wanted to delete them, so I had to go the other way. I put ‘IsDeleted‘ Boolean flag within those entity, and I made an Interface out of those duplicate code. So in my thought I and you know whenever we brought up, we implement, that interface, yes, we want our entity to use ‘IsDeleted‘ flag, instead of really get deleting from our database. I named that little kid ‘ISoftDeletableEntity‘.

So as you can guess my interface is like following code:

public interface ISoftDeletableEntity
{
    bool IsDeleted { get; set; }
}

Then we have to use this interface on our every single entity to mark them as ones who needs this functionality. to not getting deleted, but a change in state and to filter those item, when searching within the database.

public partial class Request : ISoftDeletableEntity
{
    public int Id { get; set; }
    public string Caption { get; set; }
    
    public sting IsDeleted { get; set; }
}

Once this is done, we got to write codes to filter these data. first we need to override OnModelCreating method in our database context file, usually called ‘AppplicationDbContext‘, and within that method we need to write code which iterate through all registered entity model, and pick those marked with our custom (in here ‘ISoftDeletableEntity‘) interface. Then add the global filter to each of them one by one within a loop block.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Interfaces;
using Models;
using Extensions;

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    [SuppressMessage("ReSharper", "UnusedMember.Global")]
    public ApplicationDbContext()
    {
    }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options):base(options)
    {
    }

    public virtual DbSet<Log> Log { get; set; }

    public virtual DbSet<Request> Request { get; set; }


    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        //Debugger.Launch();

        foreach (var entityType in builder.Model.GetEntityTypes())
        {
            //if (entityType.ClrType.GetInterfaces().Any(w => w == typeof(ISoftDeletableEntity)))
            if (typeof(ISoftDeletableEntity).IsAssignableFrom(entityType.ClrType))
            {
                builder.SetSoftDeleteFilter(entityType.ClrType);
            }
        }
    }
}

And then you will need this Extensions for the builder.SetSoftDeleteFilter(<type>) to work. to be honest, my I wanted my reflection to pass lambda method, not calling another method in which contains a lambda method. so when my mind was busy thinking about that, and I was frustrated by searching everywhere and trying any thing. That this following way catch my mind, I was like still i want the lambda to work, but my time and work pressure keep me from searching and trying more than that. Yet here’s the extension, unfortunately I didn’t keep the reference.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Interfaces;

[SuppressMessage("ReSharper", "InconsistentNaming")]
public static class EFFilterExtensions
{
    public static void SetSoftDeleteFilter(this ModelBuilder modelBuilder, Type entityType)
    {
        SetSoftDeleteFilterMethod.MakeGenericMethod(entityType)
            .Invoke(null, new object[] { modelBuilder });
    }

    static readonly MethodInfo SetSoftDeleteFilterMethod = typeof(EFFilterExtensions)
        .GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
        .Single(t => t.IsGenericMethod && t.Name == nameof(SetSoftDeleteFilter));

    private static void SetSoftDeleteFilter<TEntity>(this ModelBuilder modelBuilder)
        where TEntity : class, ISoftDeletableEntity
    {
        modelBuilder.Entity<TEntity>().Property(e=>e.IsDeleted);
        modelBuilder.Entity<TEntity>().HasQueryFilter(x => !x.IsDeleted);
    }
}

Cool, you are applied the filtering over all your entities, now, no matter how you are calling your data, directly, or through Include , they are filtered out using IsDeleted flag. Don’t worry you still can ignore the filtering in case of need. Just call your query as following:

var allRecords = _context.Request.IgnoreQueryFilters().ToList();

Now, you need to update your model, change it’s IsDeleted state to true , every time you want to delete it, and not just calling Remove(<entity>) method. Like:

public void DeleteItem(Request request)
{
    request.IsDeleted = true;
    return _context.Update(request);
}

But no one want to remember that they should update the model instead of deleting them, and only some can remember which entity type is delete-able. So, I give you some idea to go with, but since my own system / project is based on “Onion Architecture”, so none are tested, but hopefully they’ll give you the idea.

First thing you can try is to override the Remove(<entity>) method of the DbContext, in my case IdentityDbContext inside our ApplicationDbContext .

public override EntityEntry<TEntity> Remove<TEntity>(TEntity entity)
{
    if (entity is ISoftDeletableEntity deletableEntity)
    {
        deletableEntity.IsDeleted = true;
        return base.Update(entity);
    }
    else
    {
        return base.Remove(entity);
    }
}

As you can see we check the entity, if it implements ISoftDeletableEntity we update the model, if it’s not we use the delete functionality.

Second Scenario which i’m less sure about, and harder to write, is to override SaveChange method, and check for entity which their state imply that they are getting deleted, then set the state to update and then save.

There are four version of SaveChange methods:

public override int SaveChanges() {}
public override int SaveChanges(bool acceptAllChangesOnSuccess) {}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) {}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())

Be sure to override these two, and the other two will rely on these:

public override int SaveChanges(bool acceptAllChangesOnSuccess) {}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken()) {}

So you need to find a code working such as this, or something like it:

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
    foreach (var entityEntry in entityEntries)
    {
        if (entityEntry.State == EntityState.Deleted && typeof(ISoftDeletableEntity).IsAssignableFrom(entityEntry.Entity.GetType())
        {
            ((ISoftDeletableEntity)entityEntry.Entity).IsDeleted = true;
            entityEntry.State = EntityState.Modified;
        }
    }
    base.SaveChanges(acceptAllChangesOnSuccess); 
}

Third is to write a custom function or extension, and call that instead of directly calling the Remove method.

 

but in my Scenario, i have a generic repository, and that repository implements a delete functionality, which is same to the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Samin.Data.Repository
{
    public class Repository<T> : IRepository<T> where T : class
    {
        private readonly ApplicationDbContext _context;
        private readonly DbSet<T> _dbSet;

        public Repository(ApplicationDbContext context)
        {
            _context = context;
            _dbSet = context.Set<T>();
        }

        public virtual IQueryable<T> GetQuery()
        {
            return _dbSet;
        }

        public virtual IEnumerable<T> Get(Expression<Func<T, bool>> where = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includes = "")
        {
            var query = GetFilter(@where, orderBy, includes);
            return query.ToList();
        }

        public virtual async Task<IEnumerable<T>> GetAsync(Expression<Func<T, bool>> @where = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includes = "")
        {
            var query = GetFilter(@where, orderBy, includes);
            return await query.ToListAsync();
        }

        public virtual T GetById(object id)
        {
            return _dbSet.Find(id);
        }

        public virtual async Task<T> GetByIdAsync(object id)
        {
            return await _dbSet.FindAsync(id);
        }

        public virtual void Insert(T entity)
        {
            _dbSet.Add(entity);
        }

        public virtual void Update(T entity)
        {
            _dbSet.Update(entity);
        }

        public virtual void Delete(T entity)
        {
            if (entity is ISoftDeletableEntity baseEntity)
            {
                baseEntity.IsDeleted = true;
                Update(entity);
            }
            else
            {
                //Hard delete
                _dbSet.Remove(entity);
            }
        }

        public virtual void DeleteById(object id)
        {
            var entity = GetById(id);
            Delete(entity);
        }

        public virtual bool Exists(object id)
        {
            return _dbSet.Find(id)!=null;
        }

        public virtual async Task<bool> ExistsAsync(object id)
        {
            return await _dbSet.FindAsync(id) != null;
        }

        #region Utilities
        private IQueryable<T> GetFilter(Expression<Func<T, bool>> @where, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy, string includes)
        {
            IQueryable<T> query = _dbSet;

            if (@where != null)
            {
                query = query.Where(@where);
            }

            if (includes != "")
            {
                foreach (var include in includes.Split(','))
                {
                    query = query.Include(include);
                }
            }

            if (orderBy != null)
            {
                query = orderBy(query);
            }

            return query;
        }
        #endregion
    }
}

 

So with that you can put a global filter (which mostly is used for Soft Delete scenario), and also global delete management needed for your Soft Delete.

I hope the time you spend reading wasn’t in vain, and I hope it take a burden off your shoulders.

 

Thank you,
Hassan Faghihi.

Leave a Reply

Your email address will not be published. Required fields are marked *.

*
*
You may use these <abbr title="HyperText Markup Language">HTML</abbr> tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>