So what's changed?
Not much. Defining your query properties has changed (and obviously some implementation details). All the linq syntax stuff stays the same.
Here's the same Loan class, rewritten:
class Loan
{
[OverdueFee]
public decimal OverdueFee
{
get { return OverdueFeeAttribute.Property.GetValue(this); }
}
[IsOverdue]
public bool IsOverdue
{
get { return IsOverdueAttribute.Property.GetValue(this); }
}
[DaysOverdue]
public double DaysOverdue
{
get { return DaysOverdueAttribute.Property.GetValue(this); }
}
}
And here's our query property definition:
class IsOverdueAttribute : QueryPropertyAttribute
{
public IsOverdueAttribute() : base(Property) { }
public static readonly QueryProperty<Loan, bool> Property = new QueryProperty<Loan,bool>(
x => (x.DateReturned == null && (DateTime.Now - x.DateBorrowed).TotalDays > x.LoanPeriod));
}
class OverdueFeeAttribute : QueryPropertyAttribute
{
public OverdueFeeAttribute() : base(Property) { }
public static readonly QueryProperty<Loan, decimal> Property = new QueryProperty<Loan, decimal>(
x => (decimal)(x.DaysOverdue * 0.45));
}
class DaysOverdueAttribute : QueryPropertyAttribute
{
public DaysOverdueAttribute() : base(Property) { }
public static readonly QueryProperty<Loan, double> Property = new QueryProperty<Loan, double>(
x => x.IsOverdue ? (DateTime.Now - x.DateBorrowed).TotalDays - x.LoanPeriod : 0);
}
What's the difference?
Well, depending on your programming tastes, it's cleaner. We've reduced some of the tight coupling, and we now have the ability to spread the logic (which is more suitable in certain situations).
And if you're wondering why I seem to have an obsession with static members, it's mainly due to performance - and truth be told, I'm pre-optimizing here. Generally I'm not such a fan... honest!
Performance anxiety
Speaking of performance, using Attributes has a penalty. According to my tests, it's roughly 30% slower to parse the expression.
This might sound like a lot, but we're talking about 0.37 vs 0.28 milliseconds per query here - and once you consider the database retrieval time... it's really nothing.
I don't even parse the expression tree with linq to objects - so no impact at all!
Bonus code (Expression Parser)
Here's my expression parsing code, I know you want to see it:
class QueryPropertyEvaluator : ExpressionVisitor
{
protected override Expression VisitMemberAccess(MemberExpression m)
{
if (m.Expression != null)
{
var attribute = Attribute.GetCustomAttribute(m.Member, typeof(QueryPropertyAttribute)) as QueryPropertyAttribute;
if (attribute != null)
return Expression.Invoke(attribute.Property.GetExpression(), m.Expression);
}
return base.VisitMemberAccess(m);
}
}
Beautiful isn't it!
Feedback please
If you've been following this little series, I'd love to hear what you think of the new syntax.
I'm still hesitant, since this new syntax results in more code - but I think it's a cleaner approach.
As usual, you can find the sample app here - enjoy!
7 comments:
Think this one is a winner! Looks great dude.
Cheers Nick!
Agree, the attribute approach looks much better - well worth the slight performance hit (and I'm sure you can do some caching if its needed) :)
Does your solution work with composite queries? like a QueryProperty that internally uses other QueryProperties. For example:
public static readonly QueryProperty[Loan, bool] Property = new QueryProperty[Loan,bool](
x => (x.IsOverdue && x.Book.WaitingList.Count > 0)
Also, do you have any thoughts on parameters? For example, say I want to introduce a configurable grace period for IsOverdue.
I'm working on these two things for my specification pattern approach at the moment, and its a tough nut to crack :)
Thanks Fredrik!
My solution should fully support composite queries, since I recursively parse the expression tree.
It works great with local class members (ie. x.IsOverdue), but I haven't tested with it with child QueryProperty members yet though. (ie. x.Book.Etc)
In terms of parameters, I was thinking to support them by returning a function:
Func[DateTime, bool] IsOverdue;
QueryProperty[Loan, Func[DateTime, bool]];
But I haven't looked into how this will change the parsing routines.
I'm sure that one will give me some headaches!
class IsOverdueByAttribute : QueryMethod<Func<Order,TimeSpan,bool>>
{
// Base accepts Expression<T>
public IsOverdueByAttribute()
: base((o,t) => o.DueDate - DateTime.Now > t)
{
}
}
[IsOverdueBy]
public bool IsOverdueBy(TimeSpan time)
{
return IsOverdueByAttribute.Method(this, time);
}
Something like that perhaps? Not sure how the implementation would look.... :)
That's practically what I was thinking Nick, except I was lazy by returning a delegate instead of using a proper method.
I've actually just got parameters working with my QueryProperty's (using the lambda expression as the property type).
Modifying the expression tree was quite painful - but came good in the end.
It supports both multiple parameters [(x,y) => x+y] and nested functions [x => y => x+y], just for completeness.
I might have a look at supporting Method calls like you've written Nick - would be a nice abstraction.
Okay! I just implemented the MethodCall abstraction support in the expression parsing code.
This thing is really flexible now! You can even pass through QueryProperty's as parameters!
I love this stuff :)
Post a Comment