在表达式树中查找对DbFunction的引用并替换为不同的函数

时间:2014-08-12 04:38:50

标签: c# expression-trees

我希望在一个lambda表达式中保留一些复杂的逻辑,这些表达式可以编译,因此可以在Linq-To-Objects中使用,或者用作表达式来运行Linq-To-Entities中的数据库。

它涉及日期计算,到目前为止我一直在使用(非常简化)

之类的东西
public static Expression<Func<IParticipant, DataRequiredOption>> GetDataRequiredExpression()
{
    DateTime twentyEightPrior = DateTime.Now.AddDays(-28);
    return p=> (p.DateTimeBirth > twentyEightPrior)
        ?DataRequiredOption.Lots
        :DataRequiredOption.NotMuchYet
}

然后在类上有一个方法

public DataRequiredOption RecalculateDataRequired()
{
    return GetDataRequiredExpression().Compile()(this);
}

编译表达式树有一些开销。当然我不能简单地使用

public static Expression<Func<IParticipant, DataRequiredOption>> GetDataRequiredExpression(DateTime? dt28Prior=null)
{
    return p=> DbFunctions.DiffDays(p.DateTimeBirth, DateTime.Now) > 28
        ?DataRequiredOption.Lots
        :DataRequiredOption.NotMuchYet
}

因为这只会在数据库中运行(它会在执行Compile()方法时抛出错误)。

我不太熟悉修改表达式(或ExpressionVisitor类)。是否可能,如果是这样,我如何在表达式树中找到DbFunctions.DiffDays函数并将其替换为不同的委托?感谢您的专业知识。

修改

使用svick的精彩回应 - 稍微修改因为diffdays和date subttraction他们的参数在两种情况下切换为产生正数:

static ParticipantBaseModel()
{
    DataRequiredExpression = p => 
        ((p.OutcomeAt28Days >= OutcomeAt28DaysOption.DischargedBefore28Days && !p.DischargeDateTime.HasValue)
                    || (DeathOrLastContactRequiredIf.Contains(p.OutcomeAt28Days) && (p.DeathOrLastContactDateTime == null || (KnownDeadOutcomes.Contains(p.OutcomeAt28Days) && p.CauseOfDeath == CauseOfDeathOption.Missing))))
                ? DataRequiredOption.DetailsMissing
                : (p.TrialArm != RandomisationArm.Control && !p.VaccinesAdministered.Any(v => DataContextInitialiser.BcgVaccineIds.Contains(v.VaccineId)))
                    ? DataRequiredOption.BcgDataRequired
                    : (p.OutcomeAt28Days == OutcomeAt28DaysOption.Missing)
                        ? DbFunctions.DiffDays(p.DateTimeBirth, DateTime.Now) < 28
                            ? DataRequiredOption.AwaitingOutcomeOr28
                            : DataRequiredOption.OutcomeRequired
                        : DataRequiredOption.Complete;
    var visitor = new ReplaceMethodCallVisitor(
        typeof(DbFunctions).GetMethod("DiffDays", BindingFlags.Static | BindingFlags.Public, null, new Type[]{ typeof(DateTime?), typeof(DateTime?)},null),
        args => 
            Expression.Property(Expression.Subtract(args[1], args[0]), "Days"));
    DataRequiredFunc = ((Expression<Func<IParticipant, DataRequiredOption>>)visitor.Visit(DataRequiredExpression)).Compile();
}

1 个答案:

答案 0 :(得分:1)

使用ExpressionVisitor将静态方法的调用替换为其他内容相对简单:覆盖VisitMethodCall(),在其中检查它是否是您要查找的方法,如果是,请替换它:

class ReplaceMethodCallVisitor : ExpressionVisitor
{
    readonly MethodInfo methodToReplace;
    readonly Func<IReadOnlyList<Expression>, Expression> replacementFunction;

    public ReplaceMethodCallVisitor(
        MethodInfo methodToReplace,
        Func<IReadOnlyList<Expression>, Expression> replacementFunction)
    {
        this.methodToReplace = methodToReplace;
        this.replacementFunction = replacementFunction;
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method == methodToReplace)
            return replacementFunction(node.Arguments);

        return base.VisitMethodCall(node);
    }
}

问题是这对你不适用,因为DbFunctions.DiffDays()使用可空值。这意味着它的参数和结果都可以为空,replacementFunction必须处理所有这些:

var visitor = new ReplaceMethodCallVisitor(
    diffDaysMethod,
    args => Expression.Convert(
        Expression.Property(
            Expression.Property(Expression.Subtract(args[0], args[1]), "Value"),
            "Days"),
        typeof(int?)));
var replacedExpression = visitor.Visit(GetDataRequiredExpression());

为了让它更好地工作,你可以通过从方法参数中删除它然后在必要时将其读取到结果来改进访问者以照顾你的可空性:

protected override Expression VisitMethodCall(MethodCallExpression node)
{
    if (node.Method == methodToReplace)
    {
        var replacement = replacementFunction(
            node.Arguments.Select(StripNullable).ToList());

        if (replacement.Type != node.Type)
            return Expression.Convert(replacement, node.Type);
    }

    return base.VisitMethodCall(node);
}

private static Expression StripNullable(Expression e)
{
    var unaryExpression = e as UnaryExpression;

    if (unaryExpression != null && e.NodeType == ExpressionType.Convert
        && unaryExpression.Operand.Type == Nullable.GetUnderlyingType(e.Type))
    {
        return unaryExpression.Operand;
    }

    return e;
}

使用此功能,替换功能变得更加合理:

var visitor = new ReplaceMethodCallVisitor(
    diffDaysMethod,
    args => Expression.Property(Expression.Subtract(args[0], args[1]), "Days"));