This
should be the last part in my
series of posts. In this part I'll cover support for parameters via method calls.
Parameter Support (Contrived Example)Continuing the Library example, how would you query the books that will be overdue next week?
Well you could write the following:
var nextWeek = from a in repository.AsQueryable()
where a.CheckOverdue(DateTime.Today.AddDays(7))
select a;
How it worksThe CheckOverdue Property simply returns a Lambda Expression as it's result.
class CheckOverdueProperty : QueryProperty<Loan, Func<DateTime, bool>>
{
public CheckOverdueProperty()
: base(x => y => (x.DateReturned == null && (y - x.DateBorrowed).TotalDays > x.LoanPeriod))
{ }
}
I've modified the expression parsing routines to support parameters (including multiple) and even nested lambdas!
Nested Lambdas?I can't really see the need for it, but hey... why not! Curry anyone? :)
class MyProperty : QueryProperty<Loan, Func<int, Func<int, Func<int, double>>>>
{
public MyProperty()
: base(a => x => y => z => x + y + z + a.LoanPeriod)
{ }
}
So how would you call such a monstrosity?
var test = from a in repository.AsQueryable()
select a.MyProperty(5)(4)(3);
But check out the SQL! Great work Linq to Sql guys!
SELECT (CONVERT(Float,5 + 4 + 3)) + [t0].[LoanPeriod] AS [value]
FROM [dbo].[Loan] AS [t0]
Declaration SyntaxIf you're particularly astute, you'll have noticed that my syntax for declaring a QueryProperty has changed slightly since the last post.
Here's the class declaration of the CheckOverdue Property:
[QueryProperty(typeof(CheckOverdueProperty))]
public bool CheckOverdue(DateTime date)
{
return CheckOverdueProperty.GetValue<CheckOverdueProperty>(this)(date);
}
Although this is a method, you can still define it as a property (notice that I return a lambda expression).
[QueryProperty(typeof(CheckOverdueProperty))]
public Func<DateTime, bool> CheckOverdue
{
get { return CheckOverdueProperty.GetValue<CheckOverdueProperty>(this); }
}
So What's Changed?Well, QueryProperties no longer derive from an Attribute class. Although it worked, I felt dirty about it.
I borrowed from
Fredrik's syntax and pass the property type to the Attribute - much cleaner.
Since I'm no longer defining the QueryProperty as static, I had to change the way it is referenced by the class property.
The code above shows my attempt at making this easy, yet efficient - but it's up to you how to implement it. E.g. this would work well too:
[QueryProperty(typeof(CheckOverdueProperty))]
public Func<DateTime, bool> CheckOverdue
{
get { return CheckOverdueProperty.Value(this); }
}
private static CheckOverdueProperty CheckOverdueProperty = new CheckOverdueProperty();
If you can think of better ways I'd love to hear it!
Expression ParserSince I posted the parser code in my last post, here it is again - and my has it grown!
class QueryPropertyEvaluator : ExpressionVisitor
{
// Basic member access support for QueryProperties
protected override Expression VisitMemberAccess(MemberExpression m)
{
if (m.Expression != null)
{
var property = GetProperty(m.Member);
if (property != null)
return Expression.Invoke(property.GetExpression(), m.Expression);
}
return base.VisitMemberAccess(m);
}
// Support method call abstractions over QueryProperties
// Assumes method call has same parameters (in order!) as QueryProperty
// Also supports static methods, as long as first parameter is the class object
protected override Expression VisitMethodCall(MethodCallExpression m)
{
Expression obj = this.Visit(m.Object);
IEnumerable<Expression> args = this.VisitExpressionList(m.Arguments);
var property = GetProperty(m.Method);
if (property != null)
{
if (obj != null)
{
// Method on QueryProperty class
return VisitLambdaInvocation(Expression.Invoke(property.GetExpression(), obj), args);
}
else
{
// Assumes static method where first parameter is object instance
return VisitLambdaInvocation(Expression.Invoke(property.GetExpression(), args.First()), args.Skip(1));
}
}
if (obj != m.Object || args != m.Arguments)
return Expression.Call(obj, m.Method, args);
return m;
}
// Support lambda expressions within QueryProperties
protected override Expression VisitInvocation(InvocationExpression iv)
{
IEnumerable<Expression> args = this.VisitExpressionList(iv.Arguments);
Expression expr = this.Visit(iv.Expression);
// Rewrite InvokeExpression - Folds out inner lambda
if (expr.NodeType == ExpressionType.Invoke)
return VisitLambdaInvocation((InvocationExpression)expr, args);
if (args != iv.Arguments || expr != iv.Expression)
return Expression.Invoke(expr, args);
return iv;
}
// Recursively 'flattens' the lambda invocation expressions
// Rewrites the nested lambda expression tree to work with Linq to SQL
protected Expression VisitLambdaInvocation(Expression expression, IEnumerable<Expression> args)
{
if (expression.NodeType != ExpressionType.Invoke)
return Expression.Invoke(expression, args);
var invoke = (InvocationExpression)expression;
var lambda = (LambdaExpression)invoke.Expression;
// Func<TClass, Func<Arg0, Arg1..., Result>> -> Func<TClass, Result>
var arguments = lambda.Type.GetGenericArguments().ToArray();
arguments[arguments.Length - 1] = arguments.Last().GetGenericArguments().Last();
return Expression.Invoke(
Expression.Lambda(
lambda.Type.GetGenericTypeDefinition().MakeGenericType(arguments),
VisitLambdaInvocation(lambda.Body, args),
lambda.Parameters),
invoke.Arguments);
}
#region MemberInfo caching
private Dictionary<MemberInfo, QueryProperty> lookup = new Dictionary<MemberInfo, QueryProperty>();
// This is not strictly needed, but adds a performance improvement.
// Feel free to remove this code if you think memory is more important
protected QueryProperty GetProperty(MemberInfo info)
{
QueryProperty result = null;
if (lookup.TryGetValue(info, out result) == false)
{
var attribute = Attribute.GetCustomAttribute(info, typeof(QueryPropertyAttribute)) as QueryPropertyAttribute;
if (attribute != null)
lookup.Add(info, result = attribute.GetProperty());
}
return result;
}
#endregion
}
ConclusionAs usual, here is the
sample code for you to experiment with yourself.
I hope you've enjoyed this series, I certainly have.
If I get the chance I might write a post explaining the expression parsing in detail, or the compiled queries - but till then...
Cheers!