7

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?

Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • You can get Expression from incoming Select method, and then pass it to ViewSet Select, or compile it as Func and pass it there. So you will just resend projection to your viewSet. – Sergey Litvinov Jun 15 '16 at 21:04
  • @SergeyLitvinov ok... so how do I get an `Expression>` from that `MethodCallExpression`? – Mathieu Guindon Jun 15 '16 at 21:11
  • Also if I don't have a handle on the `Func`'s return type, how can I cast it to the correct `Expression` so as to be able to access the `Compile` method and keep things strongly-typed so that `.Select(convertedExpression)` can work? – Mathieu Guindon Jun 15 '16 at 21:29
  • As for first question about getting it - here is working example - https://dotnetfiddle.net/vPno92 . As for second - it depends... if type is the same, then it should work. Otherwise might require additional steps – Sergey Litvinov Jun 15 '16 at 21:53
  • Okay, but I don't **know** that I'm working with a `string`. In fact I can't know the return type of the `Func` until runtime... – Mathieu Guindon Jun 15 '16 at 21:54
  • Sure, but you have `TEntity` that can be used for cast. If this is Select, then it might be `Expression>`, but only when Select returns the same type which is rare thing. Otherwise you need to travel through Select body expression to get what is actual type is used there. – Sergey Litvinov Jun 15 '16 at 21:56
  • There is nothing `IQueryable` with LINQ to Objects. Everything is `IEnumerable`. Hence no need to ever bother passing in expression parameters. There is nothing wrong though with using expressions to build delegates. – leppie Jun 15 '16 at 22:07
  • 1
    Here is sample how you can get needed Select call in runtime with specific type via reflection - https://dotnetfiddle.net/1ooi2A . I've used `IQueryable`, but it should be the same for `IEnumerable` too with a couple of type changes – Sergey Litvinov Jun 15 '16 at 22:19
  • 1
    @SergeyLitvinov that's very very close! Except for some reason `method.Arguments[1] as LamdaExpression` returns `null` - `var lambda = ((UnaryExpression)method.Arguments[1]).Operand as LambdaExpression;` does it :) – Mathieu Guindon Jun 15 '16 at 22:28
  • 1
    @SergeyLitvinov it worked!! I'll post a self-answer tomorrow if you haven't posted yours by then - thanks a million! – Mathieu Guindon Jun 15 '16 at 22:35

1 Answers1

3

(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().

Community
  • 1
  • 1
Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235