Wednesday, July 23, 2008

Advanced Domain Model queries using Linq - Part IV

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 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!

3 comments:

Unknown said...

First, I must say this is excellent work.

I've been poking through your sample code and just had one question in regards to how you integrated with LinqKit.

I notice that you added a generic parameter in ExpandableQuery for the query provider, but the provider for QueryProperty does not inherit from ExpandableQueryProvider. This of course does not permit the use of Compile and Invoke.

Do you recommend that I call WithProvider twice? Once for QueryProperty and then again for ExpandableQuery.

Or would having QueryProperty provider inherit from ExpandableQueryProvider do the trick?

Thanks again,

Kavan

Luke Marshall said...

Thanks Kavan!

You are correct; I didn't inherit from ExpandableQueryProvider only because in most cases it's not needed - and it adds a slight performance overhead.

If you look at the RepositoryDatabase class in the main app, you'll see that I called WithProvider twice (ie query.Expand().ExpandWithProvider()) for the compiled query support.

If you need to use Compile & Invoke often, then inheriting from ExpandableQueryProvider will work great for you.

Either solution will work well, so feel free to use whatever method you like.

All the best!

Cheers,
Luke

Anonymous said...

I'd like to nest your lambda.