lundi 13 novembre 2023

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

I am using EF Core to build a certain searching mechanism. I am providing a generic filter that is built in the front end like this for ex.: {"filter":{"logic":"and","filters":[{"field":"ObjectMaterial.MaterialId","value":"1","operator":"Eq"}, {"field":"ObjectMaterial.MaterialId","value":"2","operator":"Eq"}]},"sort":[],"take":20,"skip":0}

Now it is using reflection to build a LINQ query based on this filter, the problem now is that ObjectMaterial is a Collection based on the classes (simplified):

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; }
}

Whenever trying to filter based on the MaterialId, the program wont run and gives errors some way or another. The code below is where I am at right now. I want to achieve a LINQ query like this: ctx.VwObjectOverview.Where(o => o.ObjectMaterial.Any(om => om.MaterialId == 1 && om.MaterialId == 2)) for ex

    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;
        }
    }




Aucun commentaire:

Enregistrer un commentaire