Reusing parts of LINQ queries is easily done by chaining methods returning IQueryable
. Unfortunately that doesn’t cover some senarios like reusing member initialization lists. To handle such a situation I wrote a Merge
extension method for select expressions.
For this post we’ll load the data required for a list of cars and a detail view for one car. When loading the details, more fields are required than when just loading the list. With the merging helper method we can reuse the query for the basic list and just extend it with the added properties.
The basic info for the list view is held in a CarBasicInfo
DTO class. The data for the detail view is held in a CarExtendedInfo
class which is derived from the CarBasicInfo
class. A helper method contains the Select()
call to map Car
entities to the basic DTO. Now the Merge
extension method can be used to merge an expression with the initialization of the additional fields for the extended DTO with the existing helper method for the basic fields.
private Expression<Func<Car, CarBasicInfo>> basicSelect = c => new CarBasicInfo { CarId = c.CarId, RegistrationNumber = c.RegistrationNumber }; var car = ctx.Cars.Select(basicSelect.Merge(c => new CarExtendedInfo { Color = c.Color, BrandName = c.Brand.Name })).Single(c => c.RegistrationNumber == "ABC123"); |
The RegistrationNumber
field is not mentioned in the init list – it is populated by the basic select in the SelectBasicInfo
helper method. To accomplish this some (non trivial) rewriting of expression trees is in the Merge
method.
The Merge code
The Merge
code makes use of two ExpressionVisitor
implementations to do the actual rewriting. The outer visitor merges the member initialization lists. The inner one is needed to rebind the base expression to the extended expression’s range variable.
/// <summary> /// Class for Merge extension methods. /// </summary> public static class LinqSelectMergeExtension { /// <summary> /// Merges the member initialization list of two lambda expressions into one. /// </summary> /// <typeparam name="TSource">Source type.</typeparam> /// <typeparam name="TBaseDest">Resulting type of the base mapping expression. TBaseDest is /// typically a super class of TExtendedDest</typeparam> /// <typeparam name="TExtendedDest">Resulting type of the extended mapping expression.</typeparam> /// <param name="baseExpression">The base mapping expression, containing a member /// initialization expression.</param> /// <param name="mergeExpression">The extended mapping expression to be merged into the /// base member initialization expression.</param> /// <returns>Resulting expression, after the merged select expression has been applied.</returns> public static Expression<Func<TSource, TExtendedDest>> Merge<TSource, TBaseDest, TExtendedDest>( this Expression<Func<TSource, TBaseDest>> baseExpression, Expression<Func<TSource, TExtendedDest>> mergeExpression) { // Use an expression visitor to perform the merge of the select expressions. var visitor = new MergingVisitor<TSource, TBaseDest, TExtendedDest>(baseExpression); return (Expression<Func<TSource, TExtendedDest>>)visitor.Visit(mergeExpression); } /// <summary> /// The merging visitor doing the actual merging work. /// </summary> /// <typeparam name="TSource">Source data type.</typeparam> /// <typeparam name="TBaseDest">Resulting type of the base query.</typeparam> /// <typeparam name="TExtendedDest">Resulting type of the merged expression.</typeparam> private class MergingVisitor<TSource, TBaseDest, TExtendedDest> : ExpressionVisitor { /// <summary> /// Internal helper, that rebinds the lambda of the base init expression. The /// reason for this is that the member initialization list of the base expression /// is bound to the range variable in the base expression. To be able to merge those /// into the extended expression, all those references have to be rebound to the /// range variable of the extended expression. That rebinding is done by this helper. /// </summary> private class LambdaRebindingVisitor : ExpressionVisitor { private ParameterExpression newParameter; private ParameterExpression oldParameter; /// <summary> /// Ctor. /// </summary> /// <param name="newParameter">The range vaiable of the extended expression.</param> /// <param name="oldParameter">The range variable of the base expression.</param> public LambdaRebindingVisitor(ParameterExpression newParameter, ParameterExpression oldParameter) { this.newParameter = newParameter; this.oldParameter = oldParameter; } /// <summary> /// Whenever a memberaccess is done that access the old parameter, rewrite /// it to access the new parameter instead. /// </summary> /// <param name="node">Member expression to visit.</param> /// <returns>Possibly rewritten member access node.</returns> protected override Expression VisitMember(MemberExpression node) { if (node.Expression == oldParameter) { return Expression.MakeMemberAccess(newParameter, node.Member); } return base.VisitMember(node); } } private MemberInitExpression baseInit; private ParameterExpression baseParameter; private ParameterExpression newParameter; /// <summary> /// Ctor /// </summary> /// <param name="baseExpression">The base expression to merge /// into the member init list of the extended expression.</param> public MergingVisitor(Expression<Func<TSource, TBaseDest>> baseExpression) { var lambda = (LambdaExpression)baseExpression; baseInit = (MemberInitExpression)lambda.Body; baseParameter = lambda.Parameters[0]; } /// <summary> /// Pick up the extended expressions range variable. /// </summary> /// <typeparam name="T">Not used</typeparam> /// <param name="node">Lambda expression node</param> /// <returns>Unmodified expression tree</returns> protected override Expression VisitLambda<T>(Expression<T> node) { if (newParameter == null) { newParameter = node.Parameters[0]; } return base.VisitLambda<T>(node); } /// <summary> /// Visit the member init expression of the extended expression. Merge the base /// expression into it. /// </summary> /// <param name="node">Member init expression node.</param> /// <returns>Merged member init expression.</returns> protected override Expression VisitMemberInit(MemberInitExpression node) { LambdaRebindingVisitor rebindVisitor = new LambdaRebindingVisitor(newParameter, baseParameter); var reboundBaseInit = (MemberInitExpression)rebindVisitor.Visit(baseInit); var mergedInitList = node.Bindings.Concat(reboundBaseInit.Bindings); return Expression.MemberInit(Expression.New(typeof(TExtendedDest)), mergedInitList); } } } |
Thank you ! IMHO, this kind of extension should be directly built in in EF !
I meant Linq not EF. By the way, I just found that this does not work if merging with more than “two levels”.
If you try to merge class with superClass and with superSuperClass you will start getting the following exception :
‘The parameter ‘YourParameterHere’ was not bound in the specified LINQ to Entities query expression’
Any idea ?
Unfortunately I don’t have any idea on that. But I can imagine that things gets more complicated if you have multiple levels to merge.
This occurs when chaining merge calls in LinqToEntities only. I’m still not sure why though. Maybe it is related to this post : http://stackoverflow.com/questions/25842275/combining-andalso-the-parameter-foo-was-not-bound-in-the-specified-linq-to-ent
I’m still investigating on what’s going on here.
I found this article and this enables some exciting possibilities, great stuff! I had an issue where I had an TypeIs expression within an init of the base expression that wasn’t being rebound, causing EF to throw. This also occurred with the Compile() method. Adding this visitor fixes that scenario.
The code that would trigger this is shown here… commenting the above visitor will cause it to throw.
https://dotnetfiddle.net/04YWR8
This is very nice implementation, thank you very much!
1. Expression<Func> selector1 = x => new { Name = x.CName };
2. Expression<Func> selector2 = x => new { FristName = x.EName };
3. Expression<Func> selector3 = x => new { Name =x.CName,FirstName=x.EName };
I want to merge 1&2 to 3 ,but create error. how to use on dynamic? thanks