ExpressionVisitor doesn't have it's VisitMethodCall invoked

945 Views Asked by At

I am following an example series on MSDN for creating a LINQ Provider and have hit a wall.

I am expecting that when I write the following test that the ExpressionVisitor sub-class in the source-code below has it's VisitMethodCall invoked.

[Fact]
public void DatabaseModeler_provides_table_modeler()
{
    var q = new LightmapQuery<AspNetRoles>(new SqliteProvider2());
    q.Where(role => role.Name == "Admin");
    var result = q.ToList();
}

What happens instead is that the VisitConstant method is invoked. I assume this is because when the Provider is instanced, it assigns it's Expression property a ConstantExpression. I'm not sure if I am doing something wrong or if the guide on MSDN has issues with it preventing me from getting the expression containing the Where method invocation.

This is the source code I have for the IQueryable<T> and IQueryProvider implementations.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Lightmap.Querying
{
    internal class SqliteQueryTranslator : ExpressionVisitor
    {
        internal StringBuilder queryBuilder;

        internal string Translate(Expression expression)
        {
            this.queryBuilder = new StringBuilder();
            this.Visit(expression);
            return this.queryBuilder.ToString();
        }

        public override Expression Visit(Expression node)
        {
            return base.Visit(node);
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.DeclaringType != typeof(IQueryable) && node.Method.Name != nameof(Enumerable.Where))
            {
                throw new NotSupportedException($"The {node.Method.Name} method is not supported.");
            }

            this.queryBuilder.Append($"SELECT * FROM {node.Method.DeclaringType.Name}");
            return node;
        }

        protected override Expression VisitConstant(ConstantExpression node)
        {
            return node;
        }

        private static Expression StripQuotes(Expression expression)
        {
            while (expression.NodeType == ExpressionType.Quote)
            {
                expression = ((UnaryExpression)expression).Operand;
            }

            return expression;
        }
    }

    public abstract class LightmapProvider : IQueryProvider
    {
        public IQueryable CreateQuery(Expression expression)
        {
            Type genericParameter = expression.GetType().GetGenericArguments().First();
            return (IQueryable)Activator.CreateInstance(typeof(LightmapQuery<>)
                .MakeGenericType(genericParameter), new object[] { this, expression });
        }

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression) => new LightmapQuery<TElement>(this, expression);

        object IQueryProvider.Execute(Expression expression)
        {
            return this.Execute(expression);
        }

        public TResult Execute<TResult>(Expression expression)
        {
            return (TResult)this.Execute(expression);
        }

        public abstract string GetQueryText(Expression expression);

        public abstract object Execute(Expression expression);
    }

    public class SqliteProvider2 : LightmapProvider
    {
        public override object Execute(Expression expression)
        {
            var x = new SqliteQueryTranslator().Translate(expression);
            return Activator.CreateInstance(typeof(List<>).MakeGenericType(TypeCache.GetGenericParameter(expression.Type, t => true)));
        }

        public override string GetQueryText(Expression expression)
        {
            throw new NotImplementedException();
        }
    }

    public class LightmapQuery<TTable> : IOrderedQueryable<TTable>
    {
        public LightmapQuery(IQueryProvider provider)
        {
            this.Provider = provider;
            this.Expression = Expression.Constant(this);
        }

        public LightmapQuery(IQueryProvider provider, Expression expression)
        {
            if (!typeof(IQueryable<TTable>).IsAssignableFrom(expression.Type))
            {
                throw new Exception();
            }

            this.Expression = expression;
            this.Provider = provider;
        }

        public Type ElementType => typeof(TTable);

        public Expression Expression { get; }

        public IQueryProvider Provider { get; }

        public IEnumerator<TTable> GetEnumerator()
        {
            return (this.Provider.Execute<IEnumerable<TTable>>(Expression)).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (Provider.Execute<IEnumerable>(Expression)).GetEnumerator();
        }
    }
}

Edit

Having updated my unit test to actually store the IQueryable from the .Where, I'm still not seeing my VisitMethodCall invoked.

[Fact]
public void DatabaseModeler_provides_table_modeler()
{
    var q = new LightmapQuery<AspNetRoles>(new SqliteProvider2())
        .Where(role => role.Name == "Admin");
    var result = q.ToList();
}
2

There are 2 best solutions below

1
On

The issue is trivial - you forgot to assign the result of applying Where:

q.Where(role => role.Name == "Admin");

use something like this instead

var q = new LightmapQuery<AspNetRoles>(new SqliteProvider2())
    .Where(role => role.Name == "Admin");
var result = q.ToList();
0
On

After some trial and error and a bit of help in IRC, I was able to identify the problem and resolve it. The issue was that my unit test project, which was a .Net Core project, did not have a reference to the System.Linq.Queryable assembly.

In my unit test

[Fact]
public void DatabaseModeler_provides_table_modeler()
{
    var q = new LightmapQuery<AspNetRoles>(new SqliteProvider2());
    var q2 = q.Where(role => role.Name == "Admin");
    var result = q2.ToList();
}

The Type of q2 is "WhereEnumerableIterator'1" which made us realize that it wasn't returning an IQueryable.

enter image description here

After adding the above reference to the project.json, the Type of q2 turned into "LightmapQuery'1" like I was expecting. With this, my VisitMethodCall method gets hit without issue.

enter image description here