4

I have an ExpressionVisitor which I add to EF Core's IQueryable<T>. Everything works fine except the Include methods. Probably because they enforce your IQueryable<T>.Provider to be an EntityQueryProvider.

Whenever I try to Include now it results in multiple queries which in turn results in the error "A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.".

How can I wire up my ExpressionVisitor so it still works with EF Core's Include functionality?

My issue is similar to this one except for EF Core instead of EF.

I hook up my ExpressionVisitor by calling it on the DbSet:

        return new Translator<TEntity>(
            _dbSet
                .AsNoTracking());

This is my Translator class:

public class Translator<T> : IOrderedQueryable<T>
{
    private readonly Expression _expression;
    private readonly TranslatorProvider<T> _provider;

    public Translator(IQueryable source)
    {
        _expression = Expression.Constant(this);
        _provider = new TranslatorProvider<T>(source);
    }

    public Translator(IQueryable source, Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        _expression = expression;
        _provider = new TranslatorProvider<T>(source);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)_provider.ExecuteEnumerable(_expression)).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _provider.ExecuteEnumerable(_expression).GetEnumerator();
    }

    public Type ElementType => typeof(T);

    public Expression Expression => _expression;

    public IQueryProvider Provider => _provider;
}

And this is my TranslatorProvider<T> class (I've taken out the non-relevant Visit methods to shorten the post):

public class TranslatorProvider<T> : ExpressionVisitor, IQueryProvider
{
    private readonly IQueryable _source;

    public TranslatorProvider(IQueryable source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        _source = source;
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        return new Translator<TElement>(_source, expression);
    }

    public IQueryable CreateQuery(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var elementType = expression.Type.GetGenericArguments().First();
        var result = (IQueryable) Activator.CreateInstance(typeof(Translator<>).MakeGenericType(elementType),
            _source, expression);
        return result;
    }

    public TResult Execute<TResult>(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var result = (this as IQueryProvider).Execute(expression);
        return (TResult) result;
    }

    public object Execute(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var translated = Visit(expression);
        return _source.Provider.Execute(translated);
    }

    internal IEnumerable ExecuteEnumerable(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var translated = Visit(expression);
        return _source.Provider.CreateQuery(translated);
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        if (node.Type == typeof(Translator<T>))
        {
            return _source.Expression;
        }
        else
        {
            return base.VisitConstant(node);
        }
    }
}
Vqf5mG96cSTT
  • 2,561
  • 3
  • 22
  • 41

1 Answers1

6

Update (EF Core 3.x):

The internal query pipeline infrastructure has changed. The new query expression preprocessing extension point is QueryTranslationPreprocessor class - Process method. Plugging it in requires replacing the IQueryTranslationPreprocessorFactory. e.g.

using System.Linq.Expressions;

namespace Microsoft.EntityFrameworkCore.Query
{
    public class CustomQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
    {
        public CustomQueryTranslationPreprocessor(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies, QueryCompilationContext queryCompilationContext)
            : base(dependencies, relationalDependencies, queryCompilationContext) { }
        public override Expression Process(Expression query) => base.Process(Preprocess(query));
        private Expression Preprocess(Expression query)
        {
            // query = new YourExpressionVisitor().Visit(query);               
            return query;
        }
    }

    public class CustomQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
    {
        public CustomQueryTranslationPreprocessorFactory(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
        {
            Dependencies = dependencies;
            RelationalDependencies = relationalDependencies;
        }
        protected QueryTranslationPreprocessorDependencies Dependencies { get; }
        protected RelationalQueryTranslationPreprocessorDependencies RelationalDependencies;
        public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
            => new CustomQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext);
    }
}

and

optionsBuilder.ReplaceService<IQueryTranslationPreprocessorFactory, CustomQueryTranslationPreprocessorFactory>();

Original:

Apparently custom query providers don't fit in the current EF Core queryable pipeline, since several methods (Include, AsNoTracking etc.) require provider to be EntityQueryProvider.

At the time of writing (EF Core 2.1.2), the query translation process involves several services - IAsyncQueryProvider, IQueryCompiler, IQueryModelGenerator and more. All they are replaceable, but the easiest place for interception I see is the IQueryModelGenerator service - ParseQuery method.

So, forget about custom IQueryable / IQueryProvider implementation, use the following class and plug your expression visitor inside Preprocess method:

using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Remotion.Linq;
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;

class CustomQueryModelGenerator : QueryModelGenerator
{
    public CustomQueryModelGenerator(INodeTypeProviderFactory nodeTypeProviderFactory, IEvaluatableExpressionFilter evaluatableExpressionFilter, ICurrentDbContext currentDbContext)
        : base(nodeTypeProviderFactory, evaluatableExpressionFilter, currentDbContext)
    { }

    public override QueryModel ParseQuery(Expression query) => base.ParseQuery(Preprocess(query));

    private Expression Preprocess(Expression query)
    {
        // return new YourExpressionVisitor().Visit(query);               
        return query;
    }
}

and replace the corresponding EF Core service inside your derived context OnConfiguring override:

optionsBuilder.ReplaceService<IQueryModelGenerator, CustomQueryModelGenerator>();

The drawback is that this is using EF Core "internal" stuff, so you should keep monitoring for changes in the future updates.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 1
    As usual, Ivan to the rescue. It requires a custom EF Core because I need to add code to the ExpressionEqualityComparer, but this is something I can live with. – Vqf5mG96cSTT Aug 28 '18 at 14:29
  • `IQueryModelGenerator` does not appear to exist any more in EF Core 3.1. Does anyone have a good general purpose alternative for where to hook into the query pipeline? – Ryan Thomas Apr 16 '20 at 16:24
  • @RyanThomas Post updated. There is also a similar factory/processor pair for postprocessing. – Ivan Stoev Apr 17 '20 at 09:49