【问题标题】:Linq dynamic expression for filtering navigation properties and collections用于过滤导航属性和集合的 Linq 动态表达式
【发布时间】:2018-01-25 01:23:30
【问题描述】:

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

我在这里尝试过this 的回答以及很多事情,但我做不到。

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

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

【问题讨论】:

  • 如果您希望动态过滤器适用于所有内容,即 IE &gt; &gt;= &lt;+ || &amp;&amp; 以及所有标准操作和 linq 操作字符串、日期功能等,那么动态过滤器将非常重要...,除此之外即使您实现了这一点,如果您不添加元数据 API,您可能会向您的用户公开大量额外数据。您可能应该考虑使用 OData 或 Dynamic Linq
  • 我有做这些操作的扩展方法。此外,我的算法适用于 CategoryDto 的 code 属性,但不适用于 Globals ......所以我需要为我的 CategoryDto 的 CultureDto 集合实现相同的@johnny5
  • 您的代码与我们在这里使用的代码非常相似。我进去后会调查
  • 谢谢你,我将不胜感激
  • 您试图在 ef 中调用 where 而不是构建表达式并将其传入

标签: c# linq reflection asp.net-core entity-framework-core


【解决方案1】:

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

当你打电话时

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

您将函数 CallWhere 传递给 EntityFramework,它会尝试解析您在 SQL 代码中调用的函数。

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

首先使用 Expression.Lambda 将您的表达式构建为它的强制类型 这会将它从 lambda Expression 转换为 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&lt;Func&lt;TEntity, bool&gt;&gt;EntityType(不是你的propertyType)

总结这一行

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

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

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

【讨论】:

  • 感谢您的帮助,但我还看不到图片。我试过了,但我无法使其可编译。
  • 终于成功了!非常感谢!我正在发布解决方案
  • @vslzl 您还有其他问题吗?
【解决方案2】:

此解决方案有效。特别感谢@(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);
    }

【讨论】:

  • 我也会在这里解决您的第二个问题,以供我查看。 typeof(Enumerable) 将实现您的 linq 结果。你想使用typeof(Queryable)
  • 是的,我改变了那条线,但性能仍然很低。过滤 20 个类别需要 1.5 秒。但是当我将此过滤器直接应用于实体(不是 DTO)时,它执行得非常快,因此由于 AutoMapper 的项目而出现性能问题。因此,我将过滤部分更改为 DataLayer,并在 projectto 方法之前过滤结果。当我改变所有这些东西时,我会通知你。 @约翰尼 5
  • 是的,我通常在转换结果之前直接在实体上工作。您的架构与我的非常相似,但我不知道您为什么使用cultureDTO 或subDTO。您没有阻止人们过滤某些属性的元数据 API 似乎也有点奇怪。由于我们的架构非常相似,如果您对未回答的问题有任何疑问,请在此处联系我,并附上链接。
  • 非常感谢你,你真是个好人,你会在我心中。我有元数据,并且正在使用属性控制可过滤的属性。上面的这些代码仅用于描述问题。原始版本大不相同且复杂。
  • 是的!使用窄带互联网仅需 100 毫秒。 DB和应用在同一台机器上会快很多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-17
  • 1970-01-01
  • 2016-11-25
  • 2017-10-22
  • 2016-11-15
相关资源
最近更新 更多