Client-side properties and any remote LINQ provider
David Fowler on the ASP.NET team and I have been bouncing ideas about how to solve an annoyance using LINQ:
If you write properties on the client, you can’t use them in remote LINQ operations.
The problem occurs because these properties can’t be translated and sent to the server. They have been compiled into Intermediate Language (IL) and not expression trees. LINQ requires expression trees to translate them using IQueryable providers. There is nothing available in .NET to let us reverse-engineer the IL back into the methods and syntax that would allow us to translate the intended operation into a remote query.
This means you end up having to write your query in two parts. Firstly, the part the server can perform. Then a ToList or AsEnumerable call to force that to happen and bring the intermediate results down to the client. Then the second part with operations that must be evaluated locally. This process can hurt performance if you want to reduce or transform the result set significantly.
What David, Colin Meek, and I came up with is a provider-independent way of declaring properties just once so they are usable in both scenarios. Computed properties for LINQ to SQL, LINQ to Entities and anything else LINQ enabled with little effort, and it works on .NET 3.5 SP1 :)
Before example
Here we have extended the Employee class to add Age and FullName. We only wanted people with “da” in their name but are forced to pull everything to the client to perform the selection.
partial class Employee {
public string FullName {
get { return Forename + " " + Surname; }
}
public int Age {
get { return DateTime.Now.Year - BirthDate.Year - (((DateTime.Now.Month < BirthDate.Month)
|| DateTime.Now.Month == BirthDate.Month && DateTime.Now.Day < BirthDate.Day) ? 1 : 0));
}
}
}
var employees = db.Employees.ToList().Where(e => e.FullName.Contains("da")).GroupBy(e => e.Age);
After example
Here, using our approach, it all happens server-side and works on both LINQ to Entities and LINQ to SQL.
partial class Employee {
private static readonly CompiledExpression<Employee,string> fullNameExpression
= DefaultTranslationOf<Employee>.Property(e => e.FullName).Is(e => e.Forename + " " + e.Surname);
private static readonly CompiledExpression<Employee,int> ageExpression
= DefaultTranslationOf<Employee>.Property(e => e.Age).Is(e => DateTime.Now.Year - e.BirthDate.Value.Year - (((DateTime.Now.Month < e.BirthDate.Value.Month) || (DateTime.Now.Month == e.BirthDate.Value.Month && DateTime.Now.Day < e.BirthDate.Value.Day)) ? 1 : 0)));
public string FullName {
get { return fullNameExpression.Evaluate(this); }
}
public int Age {
get { return ageExpression.Evaluate(this); }
}
}
var employees = db.Employees.Where(e => e.FullName.Contains("da")).GroupBy(e => e.Age).WithTranslations();
Getting started
Usage considerations
The caveats to the technique shown above are:
- You need to ensure your class is initialized before you write queries with it (see alternatives below).
- The expression you register for a property must be translatable to the remote store. You need to constrain yourself to the methods and operators your IQueryable provider supports.
There are a few alternative ways to use this rather than the specific examples above.
Registering the expressions
You can register the properties in the class itself, as shown in the examples. This means the properties themselves can evaluate the expressions without any reflection calls. Alternatively, if performance is less critical, you can register them elsewhere and have the methods look up their values dynamically via reflection. e.g.
DefaultTranslationOf<Employee>.Property(e => e.FullName).Is(e => e.Forename + " " + e.Surname);
var employees = db.Employees.Where(e => e.FullName.Contains("da")).GroupBy(e => e.Age).WithTranslations();
partial class Employee {
public string FullName { get { return DefaultTranslationOf<Employees>.Evaluate<string>(this, MethodInfo.GetCurrentMethod());} }
}
When the performance of these client-side properties is critical, you can have them as regular get properties with the full code in there at the expense of having the calculation duplicated, once in IL in the property and once as an expression for the translation.
Different maps for different scenarios
Sometimes certain parts of your application may want to run with different translations for different scenarios, performance etc. No problem!
The WithTranslations method normally operates against the default translation map (accessed with DefaultTranslationOf), but there is also another overload that takes a TranslationMap you can build for specific scenarios, e.g.
var myTranslationMap = new TranslationMap();
myTranslationMap.Add<Employees, string>(e => e.Name, e => e.FirstName + " " + e.LastName);
var results = (from e in db.Employees where e.Name.Contains("martin") select e).WithTranslations(myTranslationMap).ToList();
How it works
CompiledExpression<T, TResult>
The first thing we needed to do was get the user-written client-side “computed” properties out of IL and back into expression trees so we could translate them. Given we also want to evaluate them on the client, we need to compile them at run time, so CompiledExpression exists. It takes an expression of Func<T, TResult>, compiles it, and allows evaluation of objects against the compiled version.
ExpressiveExtensions
This little class provides both the WithTranslations extensions methods and the internal TranslatingVisitor that unravels the property accesses into their actual, registered Func<T, TResult> expressions via the TranslationMap so that the underlying LINQ provider can deal with that instead.
TranslationMap
We need to have a map of properties to compiled expressions, and for that purpose, TranslationMap exists. You can create a TranslationMap by hand and pass it to WithTranslations if you want to programmatically create them at run-time or have different ones for different scenarios, but generally, you will want to use DefaultTranslationOf.
DefaultTranslationOf
This helper class lets you register properties against the default TranslationMap we use when nothing is passed to WithTranslations. It also allows you to look-up what is already registered so you can evaluate that although there is a small reflection performance penalty:
public int Age { get { return DefaultTranslationOf<Employees>.Evaluate<int>(this, MethodInfo.GetCurrentMethod()); } }
Have fun!
[)amien
43 responses