Skip to main content
 首页 » 编程设计

c#之用于过滤导航属性和集合的 Linq 动态表达式

2024年02月25日41hnrainll

我正在尝试向我的网络 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; }         
} 

我试过了this在这里回答还有很多事情,但我做不到。

我有属性名称、操作类型(例如:contains、startswith)和来自查询字符串的比较值,因此对于各种属性名称和各种操作类型(如 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}); 
    } 
 
 
} 

异常(exception)是:

{ 
"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 87\n   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 表达式直接应用于具有上述相同扩展类的 IQueryable of CategoryDto 时

与:

[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 代码。

Entity Framework 不知道您的自定义函数。 因此,无需在表达式中调用 CallWhere,您需要构建调用 where 本身的表达式。

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

所以你想这样做:

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

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

并且由于类型安全,您将永远无法将 lambdaExpression 直接传递给 where,您只知道它是基类。所以你需要使用反射来构建表达式。

使用反射调用全局变量的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>> EntityType(不是您的 propertyType)

总结一下这一行

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

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

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