动态 Linq 表达式查询嵌套列表对象
Dynamic Linq Expression Query Nested List Object
是否可以根据 NESTED/Child 列表对象的 属性.
动态构建具有过滤条件的 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
这很好用。
但是我想查询所有宠物的名字 属性 等于“Scruffy”的人,我会发出 GET 调用,例如;
http://127.0.0.1/api/v1.0/?Filter=Pets.Name,==,Scruffy
有点出乎意料,它在 BuildPredicate 函数中的代码行中抛出以下异常。这是因为“Pets”的类型是“List”...而不是“Pet”
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,它会构建一个 'Inner Predicate' 来对子集合进行查询?即:Enumerable.Any() ?
使用原始表达式树,有时从一个示例开始,让 C# 编译器尝试一下,然后逆向工作会有所帮助。 eg;
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>
。然后使用 return 值作为参数调用 .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);
}
是否可以根据 NESTED/Child 列表对象的 属性.
动态构建具有过滤条件的 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
这很好用。
但是我想查询所有宠物的名字 属性 等于“Scruffy”的人,我会发出 GET 调用,例如;
http://127.0.0.1/api/v1.0/?Filter=Pets.Name,==,Scruffy
有点出乎意料,它在 BuildPredicate 函数中的代码行中抛出以下异常。这是因为“Pets”的类型是“List”...而不是“Pet”
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,它会构建一个 'Inner Predicate' 来对子集合进行查询?即:Enumerable.Any() ?
使用原始表达式树,有时从一个示例开始,让 C# 编译器尝试一下,然后逆向工作会有所帮助。 eg;
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>
。然后使用 return 值作为参数调用 .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);
}