Skip to content

2009 10 20 how to perform a projection in memory

Fabian Schmied edited this page Oct 20, 2009 · 1 revision

Published on September 3rd, 2009 at 14:14

How to perform a projection in-memory

For the second time, the question of how to perform custom projections using re-linq has popped up, this time on our re-motion users group. Here’s my answer to that question: unless your target query system allows you to specify complex projections (and most don't), you'll have to simulate them in memory. To do so, I'd suggest the following steps:

First, wrap the items coming from your target query system into some result object; I'll call it ResultItem. ResultItem must provide a method to access queried objects via the respective IQuerySources the object originates from:

public class ResultItem
{
  private readonly Dictionary<IQuerySource, object> _resultObjects 
      = new Dictionary<IQuerySource, object> ();

  public void Add (IQuerySource querySource, object resultObject)
  {
    _resultObjects.Add (querySource, resultObject);
  }

  public T GetObject<T> (IQuerySource source)
  {
    return (T) _resultObjects[source];
  }
}

Then, implement an expression tree visitor that builds an in-memory projector for the select expression of your QueryModel. An in-memory projector is a delegate with a signature of Func<ResultItem, T> (where T is the projection's result type) that executes the projection against a ResultItem. To build it, define a parameter expression for the ResultItem, then simply replace every QuerySourceReferenceExpression in the select expression with an expression that gets the referenced object from that parameter. Take the transformed select expression and the ResultItem parameter and compile them into a delegate; this is your in-memory projector.

Here’s the whole visitor:

// Builds an in-memory projector for a given select expression. Uses 
// ResultItems to resolve references to query sources. Does not 
// support sub-queries.
public class ProjectorBuildingExpressionTreeVisitor : ExpressionTreeVisitor
{
  // This is the generic ResultObjectMapping.GetObject<T>() method 
  // we'll use to obtain a queried object for an IQuerySource.
  private static readonly MethodInfo s_getObjectGenericMethod 
      = typeof (ResultItem).GetMethod ("GetObject");
  
  // Call this method to get the projector. T is the type of the 
  // result (after the projection).
  public static Func<ResultItem, T> BuildProjector<T> (
      Expression selectExpression)
  {
    // This is the parameter of the delegate we're building. It's the 
    // ResultItem, which holds all the input data needed for the 
    // projection.
    var resultItemParameter
        = Expression.Parameter (typeof (ResultItem), "resultItem");
    
    // The visitor gives us the projector's body. It simply replaces 
    // all QuerySourceReferenceExpressions with calls to 
    // ResultObjectMapping.GetObject<T>().
    var visitor 
        = new ProjectorBuildingExpressionTreeVisitor (resultItemParameter);
    
    var body = visitor.VisitExpression (selectExpression);
    
    // Construct a LambdaExpression from parameter and body and 
    // compile it into a delegate.
    var projector
        = Expression.Lambda<Func<ResultItem, T>> (body, resultItemParameter);
    
    return projector.Compile ();
  }
  
  private readonly ParameterExpression _resultItemParameter;
  
  private ProjectorBuildingExpressionTreeVisitor (
      ParameterExpression resultItemParameter)
  {
    _resultItemParameter = resultItemParameter;
  }
  
  protected override Expression VisitQuerySourceReferenceExpression (
      QuerySourceReferenceExpression expression)
  {
    // Substitute generic parameter "T" of ResultObjectMapping.GetObject<T>()
    // with type of query source item, then return a call to that method
    // with the query source referenced by the expression.
    var getObjectMethod
        = s_getObjectGenericMethodDefinition.MakeGenericMethod (expression.Type);
    
    return Expression.Call (
        _resultItemParameter,
        getObjectMethod,
        Expression.Constant (expression.ReferencedQuerySource));
  }
  
  protected override Expression VisitSubQueryExpression (
      SubQueryExpression expression)
  {
    throw new NotSupportedException (
        "This provider does not support subqueries in the select projection.");
  }
}

You can now apply that projector to the result items coming from the target system.

A sample application with full source code is available on GitHub.

(For discussion, please use the re-motion users group.)

- Fabian

Clone this wiki locally