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 works
The 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 Syntax
If 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 Parser
Since 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
}
Conclusion
As 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!