首页 > 解决方案 > 动态 Linq 表达式查询嵌套列表对象

问题描述

是否可以使用基于 NESTED/子列表对象属性的过滤条件动态构建 IQueryable/Linq 表达式。

我没有在这里包含所有代码——尤其是围绕分页的代码,但我希望有足够的细节。需要注意的是我使用 EFCore5 和 Automapper ProjectTo 扩展方法。

例如:


    public class PersonModel
    {
        public int Id { get; set; }
        public PersonName Name { get; set; }
        public List<Pet> Pets { get; set; }
    }

    [Owned]
    public class PersonName
    {
        public string Surname { get; set; }
        public string GivenNames { get; set; }
    }


    public class Pet
    {
        public string Name { get; set; }
        public string TypeOfAnimal { get; set; }
    }

这是我的 WebApi 控制器。

     [HttpGet(Name = nameof(GetAllPersons))]
     public async Task<ActionResult<IEnumerable<PersonDTO>>> GetAllPersons(
            [FromQuery] QueryStringParameters parameters)
        {
            IQueryable<Person> persons = _context.Persons;

            parameters.FilterClauses.ForEach(filter =>
                persons = persons.Where(filter.name, filter.op, filter.val));
            // Note the use of 'Where' Extension Method.

            var dTOs = persons
                .ProjectTo<PersonDTO>(_mapper.ConfigurationProvider);;

            var pagedPersons = PaginatedList<PersonDTO>
                .CreateAsync(dTOs, parameters);

            return Ok(await pagedPersons);

        }

要查询 Name.GivenNames 属性等于“John”的所有人,我会发出 GET 调用,例如;

http://127.0.0.1/api/v1.0/?Filter=Name.GivenNames,==,John

这工作得很好。

但是,我想查询所有拥有 Name 属性等于“Scruffy”的 Pet 的人,我会发出 GET 调用,例如;

http://127.0.0.1/api/v1.0/?Filter=Pets.Name,==,Scruffy

有点预期它会在 BuildPredicate Function 的代码行中引发以下异常。这是因为“宠物”是类型是“列表”......不是“宠物”

 var left = propertyName.Split... 

 Instance property 'Pet:Name' is not defined for type 
 System.Collections.Generic.List`1[Person]' (Parameter 'propertyName')

这是扩展方法。


public static class ExpressionExtensions
    {

        public static IQueryable<T> Where<T>(this IQueryable<T> source, string propertyName, string comparison, string value)
        {
            return source.Where(BuildPredicate<T>(propertyName, comparison, value));
        }
    }

         public static Expression<Func<T, bool>> BuildPredicate<T>(string propertyName, string comparison, string value)
        {
            var parameter = Expression.Parameter(typeof(T), "x");
            var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
            var body = MakeComparison(left, comparison, value);
            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }

        private static Expression MakeComparison(Expression left, string comparison, string value)
        {
            switch (comparison)
            {
                case "==":
                    return MakeBinary(ExpressionType.Equal, left, value);
                case "!=":
                    return MakeBinary(ExpressionType.NotEqual, left, value);
                case ">":
                    return MakeBinary(ExpressionType.GreaterThan, left, value);
                case ">=":
                    return MakeBinary(ExpressionType.GreaterThanOrEqual, left, value);
                case "<":
                    return MakeBinary(ExpressionType.LessThan, left, value);
                case "<=":
                    return MakeBinary(ExpressionType.LessThanOrEqual, left, value);
                case "Contains":
                case "StartsWith":
                case "EndsWith":
                    return Expression.Call(MakeString(left), comparison, Type.EmptyTypes, Expression.Constant(value, typeof(string)));
                default:
                    throw new NotSupportedException($"Invalid comparison operator '{comparison}'.");
            }
        }

        private static Expression MakeString(Expression source)
        {
            return source.Type == typeof(string) ? source : Expression.Call(source, "ToString", Type.EmptyTypes);
        }

        private static Expression MakeBinary(ExpressionType type, Expression left, string value)
        {
            object typedValue = value;
            if (left.Type != typeof(string))
            {
                if (string.IsNullOrEmpty(value))
                {
                    typedValue = null;
                    if (Nullable.GetUnderlyingType(left.Type) == null)
                        left = Expression.Convert(left, typeof(Nullable<>).MakeGenericType(left.Type));
                }
                else
                {
                    var valueType = Nullable.GetUnderlyingType(left.Type) ?? left.Type;                  
                    typedValue = valueType.IsEnum ? Enum.Parse(valueType, value) :
                        valueType == typeof(Guid) ? Guid.Parse(value) :
                        valueType == typeof(DateTimeOffset) ? DateTimeOffset.ParseExact(value, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal) :
                        Convert.ChangeType(value, valueType);                    
                }
            }
            var right = Expression.Constant(typedValue, left.Type);
            return Expression.MakeBinary(type, left, right);
        }



无论如何,是否可以调整此代码以检测嵌套属性之一是否是一个 LIST,它会构建一个“内部谓词”来对子集合进行查询?即: Enumerable.Any() ?

标签: c#linqdynamic-linq

解决方案


使用原始表达式树,有时从一个示例开始,让 C# 编译器试一试,然后向后工作,有时会有所帮助。例如

Expression<Func<Person,bool>> expr = p => p.Pets.Any(t => t.Foo == "blah");

尽管编译器确实在 IL 中采用了快捷方式来指定无法反编译的类型成员。

这里的诀窍是让你的方法递归。而不是假设您可以获得每个属性;

var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);

如果在列表中找到集合属性,则需要BuildPredicate<Pet>使用剩余的属性字符串进行调用。然后使用返回值作为调用的参数.Pets.Any(...)

也许是这样的;

public static Expression<Func<T, bool>> BuildPredicate<T>(string propertyName, string comparison, string value)
    => (Expression<Func<T, bool>>)BuildPredicate(typeof(T), propertyName.Split('.'), comparison, value);

public static LambdaExpression BuildPredicate(Type t, Span<string> propertyNames, string comparison, string value)
{
    var parameter= Expression.Parameter(t, "x");
    var p = (Expression)parameter;
    for(var i=0; i<propertyNames.Length; i++)
    {
        var method = p.Type.GetMethods().FirstOrDefault(m => m.Name == "GetEnumerator" && m.ReturnType.IsGenericType);
        if (method != null)
        {
            BuildPredicate(method.ReturnType.GetGenericArguments()[0], propertyNames.Slice(i), comparison, value);
            // TODO ...
        }
        else
            p = Expression.Property(p, propertyNames[i]);
    }
    // TODO ...
    return Expression.Lambda(body, parameter);
}

推荐阅读