Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make LINQ-to-Objects handle projections?

I have implemented a basic (naive?) LINQ provider that works ok for my purposes, but there's a number of quirks I'd like to address, but I'm not sure how. For example:

// performing projection with Linq-to-Objects, since Linq-to-Sage won't handle this:
var vendorCodes = context.Vendors.ToList().Select(e => e.Key);

My IQueryProvider implementation had a CreateQuery<TResult> implementation looking like this:

public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
{
    return (IQueryable<TResult>)Activator
        .CreateInstance(typeof(ViewSet<>)
        .MakeGenericType(elementType), _view, this, expression, _context);
}

Obviously this chokes when the Expression is a MethodCallExpression and TResult is a string, so I figured I'd execute the darn thing:

public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
{
    var elementType = TypeSystem.GetElementType(expression.Type);
    if (elementType == typeof(EntityBase))
    {
        Debug.Assert(elementType == typeof(TResult));
        return (IQueryable<TResult>)Activator.CreateInstance(typeof(ViewSet<>).MakeGenericType(elementType), _view, this, expression, _context);
    }

    var methodCallExpression = expression as MethodCallExpression;
    if(methodCallExpression != null && methodCallExpression.Method.Name == "Select")
    {
        return (IQueryable<TResult>)Execute(methodCallExpression);
    }

    throw new NotSupportedException(string.Format("Expression '{0}' is not supported by this provider.", expression));
}

So when I run var vendorCodes = context.Vendors.Select(e => e.Key); I end up in my private static object Execute<T>(Expression,ViewSet<T>) overload, which switches on the innermost filter expression's method name and makes the actual calls in the underlying API.

Now, in this case I'm passing the Select method call expression, so the filter expression is null and my switch block gets skipped - which is fine - where I'm stuck at is here:

var method = expression as MethodCallExpression;
if (method != null && method.Method.Name == "Select")
{
    // handle projections
    var returnType = method.Type.GenericTypeArguments[0];
    var expType = typeof (Func<,>).MakeGenericType(typeof (T), returnType);

    var body = method.Arguments[1] as Expression<Func<T,object>>;
    if (body != null)
    {
        // body is null here because it should be as Expression<Func<T,expType>>
        var compiled = body.Compile();
        return viewSet.Select(string.Empty).AsEnumerable().Select(compiled);
    }
}

What do I need to do to my MethodCallExpression in order to be able to pass it to LINQ-to-Objects' Select method? Am I even approaching this correctly?

like image 627
Mathieu Guindon Avatar asked Jun 15 '16 20:06

Mathieu Guindon


People also ask

What is a LINQ projection?

In LINQ, projection is an operation which converts an object into the new form which holds only those properties that will be subsequently used. By using projection, a developer can create a new type which is built from each object.

Which Linq operator performs a one to many element projection over a sequence?

For the projection operator we use a select in LINQ and the select operator performs a projection on the collection to select elements from the database .

What are Projections in c#?

Projection refers to the operation of transforming an object into a new form that often consists only of those properties that will be subsequently used. By using projection, you can construct a new type that is built from each object. You can project a property and perform a mathematical function on it.


1 Answers

(credits to Sergey Litvinov)

Here's the code that worked:

var method = expression as MethodCallExpression;
if (method != null && method.Method.Name == "Select")
{
    // handle projections
    var lambda = ((UnaryExpression)method.Arguments[1]).Operand as LambdaExpression;
    if (lambda != null)
    {
        var returnType = lambda.ReturnType;
        var selectMethod = typeof(Queryable).GetMethods().First(m => m.Name == "Select");
        var typedGeneric = selectMethod.MakeGenericMethod(typeof(T), returnType);
        var result = typedGeneric.Invoke(null, new object[] { viewSet.ToList().AsQueryable(), lambda }) as IEnumerable;
        return result;
    }
}

Now this:

var vendorCodes = context.Vendors.ToList().Select(e => e.Key);

Can look like this:

var vendorCodes = context.Vendors.Select(e => e.Key);

And you could even do this:

var vendors = context.Vendors.Select(e => new { e.Key, e.Name });

The key was to fetch the Select method straight from the Queryable type, make it a generic method using the lambda's returnType, and then invoke it off viewSet.ToList().AsQueryable().

like image 86
Mathieu Guindon Avatar answered Nov 12 '22 04:11

Mathieu Guindon