如何为 EF Core 创建可重用的 'Contains' 表达式

How to create a reusable 'Contains' expression for EF Core

问题

我需要使用表达式通过通用存储库与其他过滤器一起执行部分文本搜索。

当前代码状态

我有一个通用方法 returns 从我的数据库中分页结果(通过公共存储库层)。

在下面的工作示例中;

public Task<PagedResult<Person>> GetPeopleAsync(PersonSearchParams searchParams,
    PagedRequest pagedRequest = null)
{
    ParameterExpression argParam = Expression.Parameter(typeof(Locum), "locum");

    // start with a "true" expression so we have an expression to "AndAlso" with
    var alwaysTrue = Expression.Constant(true);
    var expr = Expression.Equal(alwaysTrue, alwaysTrue);

    if (searchParams != null)
    {
        BinaryExpression propExpr;

        if (searchParams.DateOfBirth.HasValue)
        {
            propExpr = GetExpression(searchParams.DateStart,
                nameof(Incident.IncidentDate), 
                argParam, 
                ExpressionType.GreaterThanOrEqual);

            expr = Expression.AndAlso(expr, propExpr);
        }

        if (searchParams.DateOfDeath.HasValue)
        {
            propExpr = GetExpression(searchParams.DateEnd,
                nameof(Incident.IncidentDate), 
                argParam, 
                ExpressionType.LessThanOrEqual);

            expr = Expression.AndAlso(expr, propExpr);
        }

        if (searchParams.BranchId.HasValue && searchParams.BranchId.Value != 0)
        {
            propExpr = GetExpression(searchParams.BranchId, 
                nameof(Incident.BranchId), argParam);

            expr = Expression.AndAlso(expr, propExpr);
        }
    }

    var lambda = Expression.Lambda<Func<Locum, bool>>(expr, argParam);
    return _unitOfWork.Repository.GetAsync(filter: lambda, pagedRequest: pagedRequest);
}

这是将我的静态 GetExpression 方法用于 Expression.EqualExpression.GreaterThanOrEqualExpression.LessThanOrEqual 查询,如下所示;

private static BinaryExpression GetExpression<TValue>(TValue value,
    string propName, ParameterExpression argParam, ExpressionType? exprType = null)
{
    BinaryExpression propExpr;

    var prop = Expression.Property(argParam, propName);
    var valueConst = Expression.Constant(value, typeof(TValue));

    switch (exprType)
    {
        case ExpressionType.GreaterThanOrEqual:
            propExpr = Expression.GreaterThanOrEqual(prop, valueConst);
            break;
        case ExpressionType.LessThanOrEqual:
            propExpr = Expression.LessThanOrEqual(prop, valueConst);
            break;
        case ExpressionType.Equal:
        default:// assume equality
            propExpr = Expression.Equal(prop, valueConst);
            break;
    }
    return propExpr;
}

注意:此代码工作正常。

问题

使用其他 SO 答案中的示例,我尝试了以下方法;

表达式

我尝试通过 Expression;

获取包含
static Expression<Func<bool>> GetContainsExpression<T>(string propertyName, 
    string propertyValue)
{
    var parameterExp = Expression.Parameter(typeof(T), "type");
    var propertyExp = Expression.Property(parameterExp, propertyName);
    MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
    var someValue = Expression.Constant(propertyValue, typeof(string));
    var containsMethodExp = Expression.Call(propertyExp, method, someValue);
    return Expression.Lambda<Func<bool>>(containsMethodExp);
}

必须将其转换为 BinaryExpression,以便使用 AndAlso 将其添加到表达式树中。我尝试将 Expressiontrue 值进行比较,但这不起作用

if (searchParams.FirstName.IsNotNullOrWhiteSpace())
{
    var propExpr = GetContainsExpression<Locum>(nameof(Locum.Firstname), 
        searchParams.FirstName);

    var binExpr = Expression.MakeBinary(ExpressionType.Equal, propExpr, propExpr);
    expr = Expression.AndAlso(expr, binExpr);
}

MethodCallExpression

我还尝试使用以下方法返回 MethodCallExpression(而不是上面的 Lambda);

static MethodCallExpression GetContainsMethodCallExpression<T>(string propertyName, 
    string propertyValue)
{
    var parameterExp = Expression.Parameter(typeof(T), "type");
    var propertyExp = Expression.Property(parameterExp, propertyName);
    MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
    var someValue = Expression.Constant(propertyValue, typeof(string));
    var containsMethodExp = Expression.Call(propertyExp, method, someValue);

    return containsMethodExp;
}

我按如下方式使用它;

if (searchParams.FirstName.IsNotNullOrWhiteSpace())
{
    var propExpr = GetContainsMethodCallExpression<Person>(nameof(Person.FirstName), 
        searchParams.FirstName);

    var binExpr = Expression.MakeBinary(ExpressionType.Equal, propExpr, alwaysTrue);
    expr = Expression.AndAlso(expr, binExpr);
}

例外情况

这些表达式被传递给一个通用方法,该方法将信息从数据库中分页出来,当我 Count 总匹配数时,在第一次执行查询时抛出异常构建的记录 query.

System.InvalidOperationException: 'The LINQ expression 'DbSet() .Where(p => True && p.FirstName.Contains("123") == True)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable ', 'AsAsyncEnumerable ', 'ToList ', or 'ToListAsync '. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'

我在分页代码中使用的 Count 方法引发了此异常。这段代码已经可以在没有任何过滤器的情况下使用,并且使用顶部描述的 ExpressionType 过滤器,所以我没有包含这段代码,因为我认为它不相关。

pagedResult.RowCount = query.Count();

This has to be converted to a BinaryExpression so it can be added to the expression tree using AndAlso

否定。不要求 Expression.AndAlso(或 Expression.OrElse 操作数 是二进制表达式(如果要求 && 的左操作数或右操作数会很奇怪或 || 始终是比较运算符)。唯一的要求是它们是 bool 返回表达式,因此 对字符串 Contains 的调用 是一个完全有效的操作数表达式。

所以首先将内部局部变量的类型从 BinaryExpression 更改为 Expression:

if (searchParams != null)
{
    Expression propExpr;
    
    // ...
}

同样的顺便说一句适用于初始表达式 - 你不需要 true == true,简单 Expression expr = Expression.Constant(true); 也会这样做。

现在您可以在类似于您发布的另一个方法(传递 ParameterExpression 并构建 属性 选择器表达式)或内联方法中发出对 string.Contains 的方法调用类似于:

if (searchParams.FirstName.IsNotNullOrWhiteSpace())
{
    var propExpr = Expression.Property(argParam, nameof(Person.FirstName));
    var valueExpr = Expression.Constant(searchParams.FirstName);
    var containsExpr = Expression.Call(
        propExpr, nameof(string.Contains), Type.EmptyTypes, valueExpr);
    expr = Expression.AndAlso(expr, containsExpr);
}