使用自定义表达式扩展 EF Core 'where' 子句

Kas*_*hov 5 c# linq entity-framework-core

我有一堆实体,它们具有定义的活动期,如“StartDate”和“EndDate”字段。大多数时候我需要查询他们根据一些自定义值检查他们的活动期。代码几乎如下所示:

public static Expression<Func<T, bool>> IsPeriodActive<T>(DateTime checkPeriodStart, DateTime checkPeriodEnd, Func<T, DateTime> entityPeriodStart, Func<T, DateTime> entityPeriodEnd) =>
    entity =>
        (checkPeriodEnd >= entityPeriodStart(entity) && checkPeriodEnd <= entityPeriodEnd(entity))
        || (checkPeriodStart >= entityPeriodStart(entity) && checkPeriodEnd <= entityPeriodEnd(entity))
        || (entityPeriodStart(entity) >= checkPeriodStart && entityPeriodStart(entity) <= checkPeriodEnd)
        || (entityPeriodEnd(entity) >= checkPeriodStart && entityPeriodEnd(entity) <= checkPeriodEnd)
        || (entityPeriodStart(entity) >= checkPeriodStart && entityPeriodStart(entity) <= checkPeriodEnd);
Run Code Online (Sandbox Code Playgroud)

问题是 Func.Invoke() 不能翻译成 SQL,这很明显。我如何扩展 EF Core 为任何实体类型添加这种“where”条件?我不能使用过滤器,因为有时我需要查询原始数据或只进行一次周期检查(不是两者),而且某些实体的这些字段名称不同。

Iva*_*oev 8

您需要将Func<T, DateTime>参数更改为Expression<Func<T, DateTime>>并将它们合并到所需的表达式中。

不幸的是,C# 编译器和 BCL 都无法帮助完成后面的任务(来自其他表达式的表达式组合)。有一些 3rd 方软件包(例如LinqKitNeinLinq等)可以解决此问题,因此如果您计划密集使用表达式组合,您可以考虑使用这些库之一。

但原理是一样的。在某些时候,自定义ExpressionVisitor用于将原始表达式的部分内容替换为另一个表达式。例如,我在这种简单的场景中使用的是创建编译时 lambda 表达式,并使用用作占位符的附加参数,然后将其替换为实际表达式,与string.Replace.

为此,我使用以下辅助方法将 lambda 表达式参数替换为另一个表达式:

public static partial class ExpressionUtils
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == Source ? Target : base.VisitParameter(node);
    }
}
Run Code Online (Sandbox Code Playgroud)

有问题的方法可能是这样的:

public static Expression<Func<T, bool>> IsPeriodActive<T>(
    DateTime checkPeriodStart,
    DateTime checkPeriodEnd,
    Expression<Func<T, DateTime>> entityPeriodStart,
    Expression<Func<T, DateTime>> entityPeriodEnd)
{
    var entityParam = Expression.Parameter(typeof(T), "entity");
    var periodStartValue = entityPeriodStart.Body
        .ReplaceParameter(entityPeriodStart.Parameters[0], entityParam);
    var periodEndValue = entityPeriodEnd.Body
        .ReplaceParameter(entityPeriodEnd.Parameters[0], entityParam);

    Expression<Func<DateTime, DateTime, bool>> baseExpr = (periodStart, periodEnd) =>
        (checkPeriodEnd >= periodStart && checkPeriodEnd <= periodEnd)
        || (checkPeriodStart >= periodStart && checkPeriodEnd <= periodEnd)
        || (periodStart >= checkPeriodStart && periodStart <= checkPeriodEnd)
        || (periodEnd >= checkPeriodStart && periodEnd <= checkPeriodEnd)
        || (periodStart >= checkPeriodStart && periodStart <= checkPeriodEnd);

    var periodStartParam = baseExpr.Parameters[0];
    var periodEndParam = baseExpr.Parameters[1];

    var expr = baseExpr.Body
        .ReplaceParameter(periodStartParam, periodStartValue)
        .ReplaceParameter(periodEndParam, periodEndValue);

    return Expression.Lambda<Func<T, bool>>(expr, entityParam);
}
Run Code Online (Sandbox Code Playgroud)

请注意,您需要将ReplaceParameter传递的表达式的主体重新绑定(使用相同的帮助器方法)Expression<Func<T, DateTime>>到要在结果表达式中使用的公共参数。

可以通过添加更多辅助方法(例如此处的Entity Framework + DayOfWeek )来简化代码,但同样,如果您计划大量使用它,更好的选择是使用一些现成的库,因为最后您将开始重新发明这些内容图书馆可以。