C# 合并/合并表达式

C# merge/combine expressions

提问人:Parsa99 提问时间:11/4/2023 更新时间:11/7/2023 访问量:70

问:

使用以下示例类(可以映射到数据库表):

class Package
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool Enabled { get; set; }

    public static Expression<Func<Package, bool>> IsActive =>
        x => x.Enabled;
}

class Schedule
{
    public DateTimeOffset Start { get; set; }
    public DateTimeOffset End { get; set; }
    public bool Enabled { get; set; }

    public static Expression<Func<Schedule, bool>> IsActive =>
        x => x.Enabled && x.Start > DateTimeOffset.Now;
}

以及以下内容(可以是联接表):

class SchedulePackage
{
    public Package Package { get; init; }
    public Schedule Schedule { get; init; }
}

如何将这两个表达式合并?活动是指其包和计划都处于活动状态(无论这意味着什么)。因此,为了避免代码重复,最好重用每个实体的逻辑。AndAlsoSchedulePackageIsActive

假设我有以下几点:

public static Expression<Func<SchedulePackage, bool>> IsActive =>
    x => x.Package.Enabled && x.Schedule.Enabled && x.Schedule.Start > DateTimeOffset.Now;

现在,如果我更新包逻辑

public static Expression<Func<Package, bool>> IsActive =>
    x => x.Enabled && x.Price > 0;

我也必须更新:SchedulePackage

public static Expression<Func<SchedulePackage, bool>> IsActive =>
    x => x.Package.Enabled && x.Package.Price > 0 && x.Schedule.Enabled && x.Schedule.Start > DateTimeOffset.Now;
C# 表达式 表达式树

评论

0赞 psnx 11/4/2023
难道不能变成布尔财产吗?例如 ?然后,将两个各自的道具放在一起就微不足道了。IsActivepublic bool IsActive => Enabled && Start > DateTimeOffset.Nowand
1赞 Servy 11/4/2023
@psnx 然后,它只会在运行时失败,因为查询提供程序不知道如何处理该属性。
1赞 dbc 11/5/2023
@Parsa99 - 它不会显示在链接的副本中,但您可以使用 ExpressionVisitor 创建 的正文的修改副本,并将参数表达式替换为其他表达式,例如访问 的表达式。有关演示,请参阅 dotnetfiddle.net/KueMiX。这回答了你的问题吗?Schedule.IsActivePackage.IsActiveSchedulePackage
1赞 Parsa99 11/5/2023
@dbc这正是我需要的。谢谢!
1赞 dbc 11/7/2023
@Parsa99 - 好的,完成了。

答:

2赞 dbc 11/7/2023 #1

在实体框架(或其他 LINQ to Entities 应用程序)的上下文中,一个表达式通常不可能调用另一个表达式,原因如“LINQ to Entities 不支持 LINQ 表达式节点类型'Invoke'”中所述 - stumped!。相反,您可以做的是子类化 ExpressionVisitor 以创建 和 的主体的修改副本,这些副本将 和 作为输入,然后使用二进制表达式组合它们以获得最终结果。Schedule.IsActivePackage.IsActiveSchedulePackage.ScheduleSchedulePackage.Package&&

为此,请首先创建以下用于组合函数表达式的扩展方法:

public static partial class ExpressionExtensions
{
    // Compose two Func<Tx, Ty> expressions with compatible generic parameters into a third.
    public static Expression<Func<T1, TResult>> Compose<T1, T2, TResult>(this Expression<Func<T2, TResult>> outer, Expression<Func<T1, T2>> inner) =>
        Expression.Lambda<Func<T1, TResult>>(
            new ParameterReplacer((outer.Parameters[0], inner.Body)).Visit(outer.Body), 
            false, inner.Parameters[0]);    

    // Compose a Func<T2, T3, TResult> expression with compatible Func<T1, T2> and Func<T1, T3> expressions to obtain a Func<T1, TResult> expression.
    public static Expression<Func<T1, TResult>> Compose<T1, T2, T3, TResult>(this Expression<Func<T2, T3, TResult>> outer, Expression<Func<T1, T2>> inner1, Expression<Func<T1, T3>> inner2)
    {
        var inner2body = new ParameterReplacer((inner2.Parameters[0], (Expression)inner1.Parameters[0])).Visit(inner2.Body);
        return Expression.Lambda<Func<T1, TResult>>(
            new ParameterReplacer((outer.Parameters[0], inner1.Body), (outer.Parameters[1], inner2body)).Visit(outer.Body), 
            false, inner1.Parameters[0]);   
    }
}

class ParameterReplacer : ExpressionVisitor
{
    // Replace formal parameters (e.g. of a lambda body) with some containing expression in scope.
    readonly Dictionary<ParameterExpression, Expression> parametersToReplace;
    public ParameterReplacer(params (ParameterExpression parameter, Expression replacement) [] parametersToReplace) =>
        this.parametersToReplace = parametersToReplace.ToDictionary(p => p.parameter, p => p.replacement);
    protected override Expression VisitParameter(ParameterExpression p) => 
        parametersToReplace.TryGetValue(p, out var e) ? e : base.VisitParameter(p);
}

现在你可以写如下:SchedulePackage.IsActive

static Lazy<Expression<Func<SchedulePackage, bool>>> IsActiveExpression = new(static () => {
    var left = Package.IsActive.Compose((SchedulePackage sp) => sp.Package);
    var right = Schedule.IsActive.Compose((SchedulePackage sp) => sp.Schedule);
    Expression<Func<bool, bool, bool>> binary = (b1, b2) => b1 && b2;
    return binary.Compose(left, right);
});

public static Expression<Func<SchedulePackage, bool>> IsActive => IsActiveExpression.Value;

生成的表达式如下所示:

sp => (sp.Package.Enabled AndAlso (sp.Schedule.Enabled AndAlso (sp.Schedule.Start > DateTimeOffset.Now)))

请注意,内部表达式已内联,而不是调用。IsActive

我懒洋洋地构造了一次表达式,并纯粹出于性能原因重用它。

在这里演示小提琴。