Home > Blockchain >  Creating a generic EF Query Filter with .NET 6
Creating a generic EF Query Filter with .NET 6

Time:02-19

So I have a filter on my DbContext like this:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        var stringList = new List<string>(); // <-- actually a service
        var guidList = new List<Guid?>(); // <-- actually a service

        modelBuilder.Entity<MyEntity>().HasQueryFilter(x =>
            (stringList.Contains(x.StringId))
                || (guidList.Contains(x.GuidId ?? Guid.Empty)));

    }

I'd like to abstract it out to something more generic like the below, but it doesn't like the params I'm passing for my filter. Any thoughts on what's causing the issue?

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.MyFilter<MyEntity>(x => x.StringId,
            x => x.GuidId);

    }


        
    public static void MyFilter<TEntity>(this ModelBuilder modelBuilder,
        Func<TEntity, string> filterOne,
        Func<TEntity, Guid?> filterTwo)
        where TEntity : class
    {
        var stringList = new List<string>(); // <-- actually a service
        var guidList = new List<Guid?>(); // <-- actually a service

        Expression<Func<TEntity, bool>> filterExpr = entity =>
            (stringList.Contains(filterOne(entity)))
                || (guidList.Contains(filterTwo(entity) ?? Guid.Empty));
        
        foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes()
                     .Where(m => m.ClrType.IsAssignableTo(typeof(TEntity))))
        {
            // modify expression to handle correct child type
            var parameter = Expression.Parameter(mutableEntityType.ClrType);
            var body = ReplacingExpressionVisitor
                .Replace(filterExpr.Parameters.First(), parameter, filterExpr.Body);
            var lambdaExpression = Expression.Lambda(body, parameter);

            // set filter
            mutableEntityType.SetQueryFilter(lambdaExpression);
        }
    }

The marked answer was 99% of the way there but technically won't work. here is a working implementation:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using System.ComponentModel.DataAnnotations;

public class Program
{
    public static void Main()
    {
        var options = new DbContextOptionsBuilder<TestDbContext>().UseInMemoryDatabase("inmemory").Options;
        var context = new TestDbContext(options);
        var fakeEntityVisible = new TestingEntity();
        fakeEntityVisible.StringId = "abc123";
        context.TestingEntities.AddRange(fakeEntityVisible, new TestingEntity(), new TestingEntity());
        context.SaveChanges();
        var something = context.TestingEntities.ToList();
        var count = something.Count;
        Console.WriteLine($"Count {count} should be 1");
    }

    public class TestingEntity
    {
        [Key]
        public Guid Id { get; set; }

        public string StringId { get; set; }
    }

    public class TestDbContext : DbContext
    {
        public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
        {
        }

        public DbSet<TestingEntity> TestingEntities { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            var stringList = new List<string>(); // <-- this is really a service
            stringList.Add("abc123");
            // THIS IS BROKEN
            modelBuilder.SimpleFilter<TestingEntity>(x => x.StringId);
        // THIS WORKS -- HOW TO ABSTRACT THIS?
        // modelBuilder.Entity<TestingEntity>().HasQueryFilter(x => stringList.Contains(x.StringId));
        }
    }
}

public static class Extensions
{
    public static void MyFilter<TEntity>(this ModelBuilder modelBuilder, Func<TEntity, string> stringId)
        where TEntity : class
    {
        var stringList = new List<string>(); // <-- this is really a service
        stringList.Add("abc123");
        Expression<Func<TEntity, bool>> filterExpr = entity => stringList.Contains(stringId(entity));
        foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes().Where(m => m.ClrType.IsAssignableTo(typeof(TEntity))))
        {
            // modify expression to handle correct child type
            var parameter = Expression.Parameter(mutableEntityType.ClrType);
            var body = ReplacingExpressionVisitor.Replace(filterExpr.Parameters.First(), parameter, filterExpr.Body);
            var lambdaExpression = Expression.Lambda(body, parameter);
            // set filter
            mutableEntityType.SetQueryFilter(lambdaExpression);
        }
    }

    public static void SimpleFilter<TEntity>(this ModelBuilder modelBuilder, Expression<Func<TEntity, string>> filterOne)
        where TEntity : class
    {
        var stringList = new List<string>(); // <-- this is really a service
        stringList.Add("abc123");
        var stringListExpr = Expression.Constant(stringList);
        foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes().Where(m => m.ClrType.IsAssignableTo(typeof(TEntity))))
        {
            var parameter = Expression.Parameter(mutableEntityType.ClrType, "e");
            var filter1 = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[]{typeof(string)}, stringListExpr, ReplacingExpressionVisitor.Replace(filterOne.Parameters[0], parameter, filterOne.Body));
            
            var body = filter1; 
            var lambdaExpression = Expression.Lambda(body, parameter);
            // set filter
            mutableEntityType.SetQueryFilter(lambdaExpression);
        }
    }
}

CodePudding user response:

Try this implementation. Sorry, not not tested:

public static void MyFilter<TEntity>(this ModelBuilder modelBuilder,
    Expression<Func<TEntity, string>> filterOne,
    Expression<Func<TEntity, Guid?>> filterTwo)
    where TEntity : class
{
    var stringList = new List<string>(); // <-- actually a service
    var guidList = new List<Guid?>(); // <-- actually a service

    var stringListExpr = Expression.Constant(stringList);
    var guidListExpr = Expression.Constant(guidList);

    foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes()
                    .Where(m => m.ClrType.IsAssignableTo(typeof(TEntity))))
    {
        var parameter = Expression.Parameter(mutableEntityType.ClrType, "e");

        // stringList.Contains(e.StrField)
        var filter1 = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new []{typeof(string)}, 
            stringListExpr,
            ReplacingExpressionVisitor.Replace(filterOne.Parameters[0], parameter, filterOne.Body));

        // guidList.Contains(e.GuidField)
        var filter2 = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new []{typeof(Guid?)}, 
            guidListExpr,
            ReplacingExpressionVisitor.Replace(filterTwo.Parameters[0], parameter, filterTwo.Body));

        // stringList.Contains(e.StrField) || guidList.Contains(e.GuidField)
        var body = Expression.OrElse(filter1, filter2);

        // e => stringList.Contains(e.StrField) || guidList.Contains(e.GuidField)
        var lambdaExpression = Expression.Lambda(body, parameter);

        // set filter
        mutableEntityType.SetQueryFilter(lambdaExpression);
    }
}
  • Related