I would like to have some somewhat complex logic kept in a single lambda expression, which can be compiled and therefore used in Linq-To-Objects, or used as an expression to run against a database in Linq-To-Entities.
it involves date calculations, and I have hitherto been using something like (hugely simplified)
public static Expression<Func<IParticipant, DataRequiredOption>> GetDataRequiredExpression()
{
DateTime twentyEightPrior = DateTime.Now.AddDays(-28);
return p=> (p.DateTimeBirth > twentyEightPrior)
?DataRequiredOption.Lots
:DataRequiredOption.NotMuchYet
}
And then having a method on a class
public DataRequiredOption RecalculateDataRequired()
{
return GetDataRequiredExpression().Compile()(this);
}
There is some overhead in compiling the expression tree. Of course I cannot simply use
public static Expression<Func<IParticipant, DataRequiredOption>> GetDataRequiredExpression(DateTime? dt28Prior=null)
{
return p=> DbFunctions.DiffDays(p.DateTimeBirth, DateTime.Now) > 28
?DataRequiredOption.Lots
:DataRequiredOption.NotMuchYet
}
Because this will only run at the database (it will throw an error at execution of the Compile() method).
I am not very familiar with modifying expressions (or the ExpressionVisitor class). Is it possible, and if so how would I find the DbFunctions.DiffDays function within the expression tree and replace it with a different delegate? Thanks for your expertise.
A brilliant response from svick was used - a slight modification beecause difdays and date subtraction have their arguments switched to produce a positive number in both cases:
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();
}
Replacing a call to a static method to something else using ExpressionVisitor
is relatively simple: override VisitMethodCall()
, in it check if it's the method that you're looking for and if it, replace it:
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);
}
}
The problem is that this won't work well for you, because DbFunctions.DiffDays()
works with nullable values. This means both its parameters and its result are nullable and the replacementFunction
would have to deal with all that:
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());
To make it work better, you could improve the visitor to take care of the nullability for you by stripping it from the method arguments and then readding it to the result, if necessary:
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;
}
Using this, the replacement function becomes much more reasonable:
var visitor = new ReplaceMethodCallVisitor(
diffDaysMethod,
args => Expression.Property(Expression.Subtract(args[0], args[1]), "Days"));