使用以下示例类(可以映射到数据库表):
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; }
}
如何使用
AndAlso
合并两个表达式?活跃的 SchedulePackage
是指其套餐和时间表都处于活跃状态(无论这些意味着什么)。所以,为了避免代码重复,最好重用每个实体的IsActive
逻辑。
假设我有以下内容:
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;
在实体框架(或其他 LINQ to Entities 应用程序)的上下文中,一个表达式通常不可能调用另一个表达式,原因请参阅“LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke” “ - 难住了!。相反,您可以做的是子类
ExpressionVisitor
来创建 Schedule.IsActive
和 Package.IsActive
主体的修改副本,将 SchedulePackage.Schedule
和 SchedulePackage.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
表达式已被内联而不是调用。
我懒惰地构造一次表达式并重复使用它纯粹是出于性能原因。
演示小提琴在这里。