在 EF Core 中使用反射筛选多对多对象属性

Using reflection to filter on a many-to-many object property in EF Core

提问人:Jasper B 提问时间:11/13/2023 最后编辑:Svyatoslav DanylivJasper B 更新时间:11/14/2023 访问量:31

问:

我正在使用 EF Core 来构建某种搜索机制。我提供了一个在前端构建的通用过滤器,如下所示:

{
  "filter": {
    "logic": "and",
    "filters": [
      { "field": "ObjectMaterial.MaterialId", "value": "1", "operator": "Eq" },
      { "field": "ObjectMaterial.MaterialId", "value": "2", "operator": "Eq" }
    ]
  },
  "sort": [],
  "take": 20,
  "skip": 0
}

现在它正在使用反射来构建基于此筛选器的 LINQ 查询,现在的问题是 ObjectMaterial 是基于类的集合(简化):

public partial class VwObjectOverview
{
    public int ObjectId { get; set; }
    public virtual ICollection<ObjectMaterial> ObjectMaterial { get; set; } = new List<ObjectMaterial>();
}

public partial class ObjectMaterial
{
    [Key]
    public int ObjectMaterialId { get; set; }

    public int MaterialId { get; set; }
}

每当尝试根据 MaterialId 进行过滤时,程序都不会运行并以某种方式出现错误。下面的代码是我现在所处的位置。我想实现这样的 LINQ 查询: 对于 exctx.VwObjectOverview.Where(o => o.ObjectMaterial.Any(om => om.MaterialId == 1 && om.MaterialId == 2))

public static class QueryableExtensions
{
    public static async Task<(IQueryable<TEntity> query, int count)> SearchWithFilter<TEntity>(this IQueryable<TEntity> query, List<SortDefinition>? sort = null, FilterDefinition? filter = null, int? skip = null, int? take = null)
    {
        if (filter != null && filter.Filters.Count > 0 && (filter.Field == null || filter.Value == null))
        {
            Expression<Func<TEntity, bool>> filterExpression = BuildFilterExpression<TEntity>(filter);
            query = query.Where(filterExpression);
        }

        var count = await query.CountAsync();

        if (sort != null)
        {
            foreach (SortDefinition sortDefinition in sort)
            {
                var parameter = Expression.Parameter(typeof(TEntity));
                var property = Expression.Property(parameter, sortDefinition.Field);
                var propAsObject = Expression.Convert(property, typeof(object));
                var lambda = Expression.Lambda<Func<TEntity, object>>(propAsObject, parameter);
                query = sortDefinition.Dir == SortDirection.Asc
                    ? query.OrderBy(lambda)
                    : query.OrderByDescending(lambda);
            }
        }

        if (skip.HasValue)
        {
            query = query.Skip(skip.Value);
        }

        if (take > 0)
        {
            query = query.Take(take.Value);
        }

        return (query, count);
    }

    private static Expression<Func<T, bool>> BuildFilterExpression<T>(FilterDefinition filterDef)
    {
        var type = typeof(T);
        var parameter = Expression.Parameter(type);
        Expression filterExpr = BuildFilterExpression<T>(filterDef, parameter);
        return Expression.Lambda<Func<T, bool>>(filterExpr, parameter);
    }

    private static Expression BuildFilterExpression<T>(FilterDefinition filterDef, Expression parameter)
    {
        if (filterDef.Filters != null && filterDef.Filters.Any())
        {
            // Build a nested filter expression by recursively calling this method on each nested filter
            var filterExpressions = filterDef.Filters
                .Select(nestedFilterDef => BuildFilterExpression<T>(nestedFilterDef, parameter))
                .ToArray();

            return filterDef.Logic == FilterLogic.And
                ? filterExpressions.Aggregate(Expression.AndAlso)
                : filterExpressions.Aggregate(Expression.OrElse);
        }

        if (filterDef.Field == null || filterDef.Value == null)
            return parameter;

        // Split the field into parts if it contains a dot (indicating a related entity property)
        var fieldParts = filterDef.Field.Split('.');
        Expression propertyExpr = GetPropertyAccessExpression(parameter, fieldParts);

        //Expression valueExpr = Expression.Constant(Convert.ChangeType(filterDef.Value, propertyExpr.Type));
        var convertedValue = Convert.ChangeType(filterDef.Value, propertyExpr.Type);
        Expression valueExpr = Expression.Constant(convertedValue);


        valueExpr = Expression.Convert(valueExpr, propertyExpr.Type);
        switch (filterDef.Operator)
        {
            case FilterOperator.Eq:
                return Expression.Equal(propertyExpr, valueExpr);
            default:
                throw new NotSupportedException($"Filter operator {filterDef.Operator} is not supported.");
        }
    }

    private static Expression GetPropertyAccessExpression(Expression parameter, string[] fieldParts)
    {
        Expression propertyAccess = parameter;

        foreach (var fieldPart in fieldParts)
        {
            // If the property is a collection, use Any to check if any element satisfies the condition
            if (propertyAccess.Type.IsGenericType && propertyAccess.Type.GetGenericTypeDefinition() == typeof(ICollection<>))
            {
                var elementType = propertyAccess.Type.GetGenericArguments()[0];
                var elementParameter = Expression.Parameter(elementType);
                var elementProperty = Expression.PropertyOrField(elementParameter, fieldPart);

                // Create a predicate to use with Any
                var predicate = Expression.Lambda(
                    BuildFilterExpression<object>(new FilterDefinition
                    {
                        Field = fieldPart,
                        Value = null, // You might need to adjust this depending on your use case
                        Operator = FilterOperator.IsNotNull
                    }, elementParameter),
                    elementParameter);

                // Use Any to check if any element satisfies the condition
                propertyAccess = Expression.Call(
                    typeof(Enumerable),
                    "Any",
                    new[] { elementType },
                    propertyAccess,
                    predicate);
            }
            else
            {
                // Access the property in the usual way
                propertyAccess = Expression.PropertyOrField(propertyAccess, fieldPart);
            }
        }

        return propertyAccess;
    }
}
C# 反射 实体框架核心

评论

0赞 Svyatoslav Danyliv 11/14/2023
实际上,我已经为任何属性路径制作了这样的函数。如果您需要助手如何使用它,请告诉我。

答: 暂无答案