用于筛选导航属性和集合的 Linq 动态表达式



我正在尝试向我的 Web API 添加过滤功能。 我有两个类作为基类

全球一是:

public abstract class GlobalDto<TKey, TCultureDtoKey, TCultureDto> :
Dto<TKey>,
IGlobalDto<TKey, TCultureDtoKey, TCultureDto>
where TCultureDto : ICultureDto<TCultureDtoKey, TKey>, new()
{
public virtual IList<TCultureDto> Globals { get; set; }        
}

而培养的那个是:

public abstract class CultureDto<TKey, TMasterDtoKey> :
SubDto<TKey, TMasterDtoKey>,
ICultureDto<TKey, TMasterDtoKey>
{
public int CultureId { get; set; }
}

SubDto 类也是:

public abstract class SubDto<TKey, TMasterDtoKey> : Dto<TKey>, ISubDto<TKey, TMasterDtoKey>
{
public TMasterDtoKey MasterId { get; set; }
}

我正在尝试的场景是动态过滤 IQueryable GlobalDto,并按其过滤

IList<TCultureDto> Globals { get; set; }

例如:

public class CategoryDto : GlobalDto<int, int, CategoryCultureDto>, IDtoWithSelfReference<int>        
{
public int? TopId { get; set; }
[StringLength(20)]
public string Code { get; set; }
public IList<CategoryCoverDto> Covers { get; set; }
}
public class CategoryCultureDto : CultureDto<int, int>
{
[Required]
[StringLength(100)]
public string Name { get; set; }        
}

我在这里尝试过这个答案,也尝试了很多东西,但我无法做到。

我有属性名称、操作类型(例如:包含、开头)和比较查询字符串中的值,因此它必须对各种属性名称和各种操作类型(如 co(contains))和无限值(如 foo))是动态的。

http://localhost:5000/categories?search=name co foo

在此请求之后

IQueryable<CategoryDto> q;//query
/* Expression building process equals to q.Where(p=>p.Globals.Any(c=>c.Name.Contains("foo")))*/
return q.Where(predicate);//filtered query

但我无法为全局者制作

编辑:我用于执行此操作的代码。

[HttpGet("/[controller]/Test")]
public IActionResult Test()
{
var propName = "Name";
var expressionProvider = new GlobalStringSearchExpressionProvider();
var value = "foo";
var op = "co";
var propertyInfo = ExpressionHelper
.GetPropertyInfo<CategoryCultureDto>(propName);
var obj = ExpressionHelper.Parameter<CategoryCultureDto>();
// Build up the LINQ expression backwards:
// query = query.Where(x => x.Property == "Value");
// x.Property
var left = ExpressionHelper.GetPropertyExpression(obj, propertyInfo);
// "Value"
var right = expressionProvider.GetValue(value);
// x.Property == "Value"
var comparisonExpression = expressionProvider
.GetComparison(left, op, right);
// x => x.Property == "Value"
var lambdaExpression = ExpressionHelper
.GetLambda<CategoryCultureDto, bool>(obj, comparisonExpression);
var q = _service.GetAll(); //this returns IQueryable<CategoryDto>
var query = q.Where(p => p.Globals.CallWhere(lambdaExpression).Any());
var list = query.ToList();
return Ok(list);
}

public class GlobalStringSearchExpressionProvider : DefaultSearchExpressionProvider
{
private const string StartsWithOperator = "sw";
private const string EndsWithOperator = "ew";
private const string ContainsOperator = "co";
private static readonly MethodInfo StartsWithMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "StartsWith" && m.GetParameters().Length == 2);
private static readonly MethodInfo EndsWithMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "EndsWith" && m.GetParameters().Length == 2);
private static readonly MethodInfo StringEqualsMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "Equals" && m.GetParameters().Length == 2);
private static readonly MethodInfo ContainsMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "Contains" && m.GetParameters().Length == 1);
private static readonly ConstantExpression IgnoreCase
= Expression.Constant(StringComparison.OrdinalIgnoreCase);
public override IEnumerable<string> GetOperators()
=> base.GetOperators()
.Concat(new[]
{
StartsWithOperator,
ContainsOperator,
EndsWithOperator
});
public override Expression GetComparison(MemberExpression left, string op, ConstantExpression right)
{
switch (op.ToLower())
{
case StartsWithOperator:
return Expression.Call(left, StartsWithMethod, right, IgnoreCase);
// TODO: This may or may not be case-insensitive, depending
// on how your database translates Contains()
case ContainsOperator:
return Expression.Call(left, ContainsMethod, right);
// Handle the "eq" operator ourselves (with a case-insensitive compare)
case EqualsOperator:
return Expression.Call(left, StringEqualsMethod, right, IgnoreCase);
case EndsWithOperator:
return Expression.Call(left, EndsWithMethod, right);
default: return base.GetComparison(left, op, right);
}
}
}

public static class ExpressionHelper
{
private static readonly MethodInfo LambdaMethod = typeof(Expression)
.GetMethods()
.First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2);
private static readonly MethodInfo[] QueryableMethods = typeof(Queryable)
.GetMethods()
.ToArray();
private static MethodInfo GetLambdaFuncBuilder(Type source, Type dest)
{
var predicateType = typeof(Func<,>).MakeGenericType(source, dest);
return LambdaMethod.MakeGenericMethod(predicateType);
}
public static PropertyInfo GetPropertyInfo<T>(string name)
=> typeof(T).GetProperties()
.Single(p => p.Name == name);
public static ParameterExpression Parameter<T>()
=> Expression.Parameter(typeof(T));
public static ParameterExpression ParameterGlobal(Type type)
=> Expression.Parameter(type);
public static MemberExpression GetPropertyExpression(ParameterExpression obj, PropertyInfo property)
=> Expression.Property(obj, property);
public static LambdaExpression GetLambda<TSource, TDest>(ParameterExpression obj, Expression arg)
=> GetLambda(typeof(TSource), typeof(TDest), obj, arg);
public static LambdaExpression GetLambda(Type source, Type dest, ParameterExpression obj, Expression arg)
{
var lambdaBuilder = GetLambdaFuncBuilder(source, dest);
return (LambdaExpression)lambdaBuilder.Invoke(null, new object[] { arg, new[] { obj } });
}
public static IQueryable<T> CallWhere<T>(this IEnumerable<T> query, LambdaExpression predicate)
{
var whereMethodBuilder = QueryableMethods
.First(x => x.Name == "Where" && x.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T));
return (IQueryable<T>)whereMethodBuilder
.Invoke(null, new object[] { query, predicate });
}
public static IQueryable<T> CallAny<T>(this IEnumerable<T> query, LambdaExpression predicate)
{
var anyMethodBuilder = QueryableMethods
.First(x => x.Name == "Any" && x.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T));
return (IQueryable<T>) anyMethodBuilder
.Invoke(null, new object[] {query, predicate});
}

}

例外情况是:

{
"message": "Could not parse expression 'p.Globals.CallWhere(Param_0 => Param_0.Name.Contains("stil"))': This overload of the method 'ImjustCore.CrossCutting.Extensions.Expressions.ExpressionHelper.CallWhere' is currently not supported.",
"detail": "   at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.GetNodeType(MethodCallExpression expressionToParse)n   at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable`1 arguments, MethodCallExpression expressionToParse)n   at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier)n   at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseNode(Expression expression, String associatedIdentifier)n   at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier)n   at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseTree(Expression expressionTree)n   at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot)n   at Remotion.Linq.Parsing.ExpressionVisitors.SubQueryFindingExpressionVisitor.Visit(Expression expression)n   at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression`1 node)n   at System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor)n   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)n   at Remotion.Linq.Parsing.ExpressionVisitors.SubQueryFindingExpressionVisitor.Visit(Expression expression)n   at Remotion.Linq.Parsing.ExpressionVisitors.SubQueryFindingExpressionVisitor.Process(Expression expressionTree, INodeTypeProvider nodeTypeProvider)n   at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.ProcessArgumentExpression(Expression argumentExpression)n   at System.Linq.Enumerable.SelectListPartitionIterator`2.ToArray()n   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)n   at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable`1 arguments, MethodCallExpression expressionToParse)n   at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier)n   at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseTree(Expression expressionTree)n   at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot)n   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](Expression query, INodeTypeProvider nodeTypeProvider, IDatabase database, IDiagnosticsLogger`1 logger, Type contextType)n   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass15_0`1.<Execute>b__0()n   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)n   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)n   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)n   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)n   at Remotion.Linq.QueryableBase`1.GetEnumerator()n   at System.Collections.Generic.List`1.AddEnumerable(IEnumerable`1 enumerable)n   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)n   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)n   at ImjustCore.Presentation.Api.Controllers.CategoriesController.Test() in /Users/apple/Desktop/Development/Core/ImjustCore/ImjustCore/ImjustCore.Presentation.Api/Controllers/CategoriesController.cs:line 87n   at lambda_method(Closure , Object , Object[] )n   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()n--- End of stack trace from previous location where exception was thrown ---n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)n   at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)n   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextActionFilterAsync>d__10.MoveNext()n--- End of stack trace from previous location where exception was thrown ---n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeInnerFilterAsync>d__14.MoveNext()n--- End of stack trace from previous location where exception was thrown ---n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)n   at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)n   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeNextExceptionFilterAsync>d__23.MoveNext()"
}

当我将 lambda 表达式直接应用于具有上述相同扩展类的CategoryDto 的 IQueryable

跟:

[HttpGet("/[controller]/Test")]
public IActionResult Test()
{
var propName = "Code";
var expressionProvider = new StringSearchExpressionProvider();
var value = "foo";
var op = "co";
var propertyInfo = ExpressionHelper
.GetPropertyInfo<CategoryDto>(propName);
var obj = ExpressionHelper.Parameter<CategoryCultureDto>();
// Build up the LINQ expression backwards:
// query = query.Where(x => x.Property == "Value");
// x.Property
var left = ExpressionHelper.GetPropertyExpression(obj, propertyInfo);
// "Value"
var right = expressionProvider.GetValue(value);
// x.Property == "Value"
var comparisonExpression = expressionProvider
.GetComparison(left, op, right);
// x => x.Property == "Value"
var lambdaExpression = ExpressionHelper
.GetLambda<CategoryDto, bool>(obj, comparisonExpression);
var q = _service.GetAll();
var query = q.CallWhere(lambdaExpression);
var list = query.ToList();
return Ok(list);
}

它工作正常。 因为没有对子集合进行筛选,并且结果正在正确筛选。

我希望这对你有用,伪编码。

当您致电时

var query = q.Where(p => p.Globals.CallWhere(lambdaExpression).Any());

你正在将函数CallWhere传递给 EntityFramework,后者会尝试将你调用的函数解析为 SQL 代码。

实体框架不知道您的自定义函数。 因此,与其在表达式中调用 CallWhere,不如构建调用 where 本身的表达式。

首先使用 Expression.Lambda 将表达式构建为其强制转换类型 这会将其强制转换为从 lambda 表达式到表达式的表达式,但由于您在运行时没有您的类型,因此您需要通过反射调用 where 子句,因为您从来没有具体的 TKey。

所以你想这样做:

var castedExpression = Expression.Lambda<Func<TKey, bool>>(lambdaExpression, lambdaExpression.Parameters);
x => x.Globals.Where(castedExpression)

但是你不能,因为你在编译时不知道 TKey,

并且由于类型安全,您将永远无法将lambdaExpression直接传递到您的位置,您只知道它是基类。 因此,您需要使用反射来构建表达式。

使用反射在全局变量上调用 where 方法

要像这样构建 lambda:

var propertyInfo = ExpressionHelper.GetProperty("globals");
var castedExpression = Expression.Lambda(typeof(propertyInfo.PropertyType), lambdaExpression, Paramters)
// now write a function which build an expression at runtime
// x => x.Globals.Where(castedExpression)

返回类型为"是你"Expression<Func<TEntity, bool>>"实体类型"(不是"属性类型")

总结一下这条线

// var query = q.Where(p => p.Globals.CallWhere(lambdaExpression).Any());

需要看起来更像这样 我们知道您需要全局属性在内部获取它

var expression = BuildGlobalExpression<CategoryDto>(lambdaExpression,  "Any")
q.Where(expression);  

这个解决方案奏效了。特别感谢@(johnny 5)的关注和支持。

[HttpGet("/[controller]/test/{searchTerm}")]
public IActionResult Test(string searchTerm)
{                     
var stringSearchProvider = new StringSearchExpressionProvider();
var cid = 1;
//turns IQueryable<CategoryDto>
var q = _service.GetAll();
//c
var parameter = Expression.Parameter(typeof(CategoryCultureDto), "c");
var property = typeof(CategoryCultureDto).GetTypeInfo().DeclaredProperties
.Single(p => p.Name == "Name");
//c.Name
var memberExpression = Expression.Property(parameter, property);
//searchTerm = Foo
var constantExpression = Expression.Constant(searchTerm);
//c.Name.Contains("Foo")
var containsExpression = stringSearchProvider.GetComparison(
memberExpression,
"co",
constantExpression);
//cultureExpression = (c.CultureId == cultureId)
var cultureProperty = typeof(CategoryCultureDto)
.GetTypeInfo()
.GetProperty("CultureId");
//c.CultureId
var cultureMemberExp = Expression.Property(parameter, cultureProperty);
//1
var cultureConstantExp = Expression.Constant(cid, typeof(int));
//c.CultureId == 1
var equalsCulture = (Expression) Expression.Equal(cultureMemberExp, cultureConstantExp);
//(c.CultureId == 1) && (c.Name.Contains("Foo"))
var bothExp = (Expression) Expression.And(equalsCulture, containsExpression);
// c => ((c.CultureId == 1) && (c.Name.Contains("Foo"))
var lambda = Expression.Lambda<Func<CategoryCultureDto, bool>>(bothExp, parameter);
//x
var categoryParam = Expression.Parameter(typeof(CategoryDto), "x");
//x.Globals.Any(c => ((c.CultureId == 1) && (c.Name.Contains("Foo")))
var finalExpression = ProcessListStatement(categoryParam, lambda);
//x => (x.Globals.Any(c => ((c.CultureId == 1) && (c.Name.Contains("Foo"))))
var finalLambda = Expression.Lambda<Func<CategoryDto, bool>>(finalExpression, categoryParam);
var query = q.Where(finalLambda);
var list = query.ToList();
return Ok(list);
}

public Expression GetMemberExpression(Expression param, string propertyName)
{
if (!propertyName.Contains(".")) return Expression.Property(param, propertyName);
var index = propertyName.IndexOf(".");
var subParam = Expression.Property(param, propertyName.Substring(0, index));
return GetMemberExpression(subParam, propertyName.Substring(index + 1));
}
private Expression ProcessListStatement(ParameterExpression param, LambdaExpression lambda)
{
//you can inject this as a parameter so you can apply this for any other list property
const string basePropertyName = "Globals";
//getting IList<>'s generic type which is CategoryCultureDto in this case
var type = param.Type.GetProperty(basePropertyName).PropertyType.GetGenericArguments()[0];
//x.Globals
var member = GetMemberExpression(param, basePropertyName);
var enumerableType = typeof(Enumerable);
var anyInfo = enumerableType.GetMethods()
.First(m => m.Name == "Any" && m.GetParameters().Length == 2);
anyInfo = anyInfo.MakeGenericMethod(type);
//x.Globals.Any(c=>((c.Name.Contains("Foo")) && (c.CultureId == cid)))
return Expression.Call(anyInfo, member, lambda);
}

最新更新