When you’re modelling a domain, it makes sense to provide attributes and methods on your domain objects to encapsulate domain logic. The alternative is the anaemic domain model anti-pattern, where the domain classes are dumb record types and domain logic is exiled to separate service classes. Most modern object relational mappers, including LightSpeed, support rich domain models: for example, in the LightSpeed designer you can create partial classes where you can add your own properties and methods that contain this domain logic.
The trouble with this simple approach is that it doesn’t play nicely with querying. Users of the domain model need to know which properties and methods are part of the persistence model and which are not. For example, suppose we have a Person class in our domain model, with FirstName, LastName and FullName properties, where FullName is computed from FirstName and LastName. Let’s write some queries:
unitOfWork.People.Where(p => p.FirstName == "Enguerrand"); // works unitOfWork.People.Where(p => p.LastName == "de Coucy"); // works unitOfWork.People.Where(p => p.FullName == "Enguerrand de Coucy"); // oh noes!
The problem of course is that in the third case the ORM needs to generate SQL along the lines of WHERE FullName = ..., which fails because there’s no FullName column — the FullName property exists only in the object realm. A similar issue applies to using FullName in ordering clauses, or to selecting FullName as part of a LINQ projection.
So the FullName property is a bit of a second-class citizen. And users of the Person class are forced to know about the database model, not just the domain model. The domain abstraction leaks like Scott Barnes after a crate of Fosters.
What we’d like is to express the FullName logic in such a way that it can be translated into SQL, and to have the ORM perform that translation for us.
The LightSpeed translation convention
The latest LightSpeed nightly builds adopt a convention by which you can specify a translation for computed properties such as FullName. The convention is that LightSpeed will look for a static field with the same name as the property with the suffix “Expression”, and of lambda expression type. Here’s an example:
// Property in Person class public string FullName { get { return FirstName + " " + LastName; } } // Query expression in Person class corresponding to FullName property // Note field type must be Expression<Func<entity_type, domain_property_type>> private static readonly Expression<Func<Person, string>> FullNameExpression = p => p.FirstName + " " + p.LastName;
As you can see, the logic of FullNameExpression is exactly the same as the way you’d normally implement the FullName getter — you just need to put it into lambda notation.
With this expression in place, when LightSpeed encounters FullName in a Where, OrderBy or Select clause, it will quietly translate it into FirstName + " " + LastName, and go on and translate that to SQL:
// C# query UnitOfWork.People.Where(p => p.FullName == "Enguerrand de Coucy"); // Generated SQL (SQL Server dialect; will vary on other platforms) SELECT -- details omitted FROM Person WHERE ((Person.FirstName + ' ') + Person.LastName) = 'Enguerrand de Coucy'
So now the FullName property works just like the FirstName and LastName properties as far as querying, sorting and projection are concerned. Users of your domain model don’t need to know that FullName isn’t a database column.
The convention also applies to domain methods, which can be useful if you have domain logic which requires additional data to evaluate a value. Again, you need a field with the “Expression” suffix and of lambda type. The first parameter of the expression is the domain object, and each method argument maps to another expression parameter. Again, this is probably clearer with an example:
// Method in Person class public int AgeInYear(int year) { return year - BirthDate.Year; } // Query expression in Person class corresponding to AgeInYear method // Notice second parameter of type int corresponding to "year" method argument private static readonly Expression<Func<Person, int, int>> AgeInYearExpression = (p, year) => year - p.BirthDate.Year; // C# query var query = unitOfWork.People.Where(p => p.AgeInYear(2020) >= 18); // Generated SQL (SQL Server dialect; will vary on other platforms) SELECT -- details omitted FROM Person WHERE (2020 - DATEPART(YEAR, Person.BirthDate)) >= 18
Of course, the expression still has to be translatable to SQL on your particular database platform. So the abstraction is far from perfect, especially when you start dealing with complex data types or complicated logic. (And some database platforms have restrictions on the kind of expressions that can be used in SELECT or ORDER BY clauses which can prevent even relatively simple logic from running server-side.) But for many simple computed domain values this technique means that your users can use arbitrary properties and methods in their LINQ queries without needing to worry about which ones are mapped directly to the database and which ones aren’t.
I see dead code
One thing you may find offensive is that we are duplicating logic across the CLR property or method and its translation expression. Code duplication is bad, right? What if we had to change the FullName or AgeInYear logic? We’d need to remember to update it in both the property getter and the expression field. Can’t we write the domain logic once and have it magically appear in both places?
Well, until the C# language team give us macro support, which we are promised will be shortly after hell freezes over, there’s no ideal solution to this. However, there is a non-ideal solution which may suffice for some scenarios. Lambda expressions support a Compile() method which compiles the expression into a CLR delegate. So if you compile, say, the FullNameExpression and call it on the this reference, you can avoid the code duplication:
public string FullName { get { return FullNameExpression.Compile()(this); } // careful now! }
As written, though, you pay a terrible price for this: the expression is recompiled every time you call the property getter. This is far, far slower than if you’d just written the duplicate code in the property getter — though still only a few microseconds for a simple expression. By caching the compilation result you can bring this down to a manageable amount, but it will still be slower than plain code. So you can improve maintainability but at the cost of performance; and as always with performance tradeoffs, you can only make the right decision by measuring the actual performance impact in your target scenarios. (As an indicator, the overhead we’re talking about might be between 10 and 100 nanoseconds depending on your caching mechanism. But don’t use my figures. Measure it!)
Yeah! I’ve had it with anaemic domain models! I want to start enriching my domain model right now!
Magic expressions are in the current LightSpeed nightly build. If you want to take them for a spin, grab the free edition from the downloads page, or the retail edition from the store. This feature is currently experimental so be sure to let us know how it works for you or if you run into any bugs! Enjoy!
This looks exactly like the post I put out last June about writing client-side properties that have a corresponding LINQ expression for querying at http://damieng.com/blog/2009/06/24/client-side-properties-and-any-remote-linq-provider
You even use the same two examples – full name and age (except your age calculation is wrong).
[)amien
Thanks for that, Damien — I hadn’t seen your post. Very nice getting it to work with all LINQ providers — if I’d known I’d have saved myself some time and just pointed people at your post! (And fair cop on the age thing — the reason for using an *ahem* approximate age calculation was to keep the expression simple so as to highlight the translation convention.)
When writing LINQ Queries, do you support ‘let’
For example, in L2S/EF you can write
var poo = from s in ctx.Customers
let fullName = string.Format(“{0} {1}”, s.FirstName, s.LastName)
where fullName.Contains(“lip”)
select s;
The current build of NHibernate (from 2 months ago when i last tested) with QueryOver currently generates the correct SQL statement but fails to execute it (you can copy paste that SQL into SSMS and it executes just fine, hopefully they fix the issue tho)
Hmm, we support let for members (let fn = s.FirstName) but it looks like we don’t support it for composite expressions like your example. I’ll take a look and see if we can address this.
Awesome.
Another example I’ve seen is with Dates, tho I’ve not tried it with NH3.
So something like:
var poo = from s in ctx.Shifts
let shiftEnd = s.Start.Add(new TimeSpan(0, s.Duration), 0)
where someDateTime >= s.Start
&& someDateTime <= shiftEnd
select s;
Both i think are probably rare bits of functionality but what ever helps make LS better :)
Hello Philip,
I’ve got a candidate implementation for your first example (except that we don’t support String.Format — you’ll have to use s.FirstName + ” ” + s.LastName — but as far as I can tell LINQ to SQL doesn’t support String.Format either) and it should be in the next nightly build.
Your second example as written is more problematic because it would require us to do a fair bit of work on the specifics of the TimeSpan class. However, you should be able to do it using AddMinutes: let shiftEnd = s.Start.AddMinutes(s.Duration). Again you will need the forthcoming nightly build for this.
The implementation of non-trivial “let” clauses is fairly basic at the moment so it may not handle all the cases you want to throw at it — please let us know in the forums when you run into cases it can’t cope with, though we’ll probably be aiming to support only simple cases at this stage.
Forgot to say, the AddMinutes trick is currently only available on SQL Server 2005 and above. If you need us to implement AddMinutes and related functions on other platforms, drop us a note in the forums.
Hey Ivan, I got around to testing it, I wrote up a comparison against LightSpeed, EF, L2S and NHibernate.
http://www.philliphaydon.com/2011/01/let-keyword-in-orms/
Thanks for the write-up and the link, Phillip. We’ll have to take a look at whether we can add support for that Count() scenario!
This is fantastic Ivan.
There’s one thing that I’d like it to do, but which didn’t seem to work. I wanted to use one of these “ninja” properties through an interface.
For instance, imagine this method:
IQueryable GetStartedEntities(IUnitOfWork uow) where T: IHasStartDate
{
return uow.Query().Where(obj => obj.StartDate > DateTime.Now);
}
It works as desired in LightSpeed when the StartDate property is a real property on the object. But, it fails when StartDate is a ninja property. I can probably send you some repro code if necessary. Email me if you’d like me to do that.
Opps, the generic symbols in my code snipped got lost. Here it is again, with { } instead:
IQueryable{T} GetStartedEntities{T}(IUnitOfWork uow) where T: IHasStartDate
{
return uow.Query{T}().Where(obj => obj.StartDate > DateTime.Now);
}
Opps, that was IQueryable of T, and Query of T
Leave a Reply