【问题标题】:Wire up ExpressionVisitor with EF Core Include使用 EF Core Include 连接 ExpressionVisitor
【发布时间】:2020-04-17 17:43:47
【问题描述】:

我有一个 ExpressionVisitor,我将它添加到 EF Core 的 IQueryable<T>。除 Include 方法外,一切正常。可能是因为他们强制你的IQueryable<T>.ProviderEntityQueryProvider

每当我现在尝试包含时,它都会导致多个查询,进而导致错误“在前一个操作完成之前在此上下文上启动了第二个操作。不保证任何实例成员都是线程安全的。”。

如何连接我的 ExpressionVisitor,使其仍可与 EF Core 的包含功能一起使用?

我的问题类似于this one,除了 EF Core 而不是 EF。

我通过在 DbSet 上调用 ExpressionVisitor 来连接它:

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

这是我的Translator 班级:

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;
}

这是我的TranslatorProvider&lt;T&gt;类(我已经去掉了不相关的访问方法来缩短帖子):

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);
        }
    }
}

【问题讨论】:

    标签: c# entity-framework-core


    【解决方案1】:

    更新(EF Core 3.x):

    内部查询管道基础架构已更改。新的查询表达式预处理扩展点是QueryTranslationPreprocessor 类-Process 方法。插入它需要更换IQueryTranslationPreprocessorFactory。例如

    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);
        }
    }
    

    optionsBuilder.ReplaceService<IQueryTranslationPreprocessorFactory, CustomQueryTranslationPreprocessorFactory>();
    

    原文:

    显然,自定义查询提供程序不适合当前的 EF Core 可查询管道,因为有几种方法(IncludeAsNoTracking 等)要求提供程序为 EntityQueryProvider

    在撰写本文时(EF Core 2.1.2),查询翻译过程涉及多个服务 - IAsyncQueryProviderIQueryCompilerIQueryModelGenerator 等等。它们都是可替换的,但我看到的最容易拦截的地方是 IQueryModelGenerator 服务 - ParseQuery 方法。

    所以,忘记自定义 IQueryable / IQueryProvider 实现,使用以下类并将您的表达式访问者插入 Preprocess 方法:

    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;
        }
    }
    

    并替换派生上下文中的相应 EF Core 服务 OnConfiguring 覆盖:

    optionsBuilder.ReplaceService<IQueryModelGenerator, CustomQueryModelGenerator>();
    

    缺点是这是使用 EF Core“内部”的东西,所以你应该继续监控未来更新中的变化。

    【讨论】:

    • 像往常一样,Ivan 来救援。它需要一个自定义的 EF Core,因为我需要向 ExpressionEqualityComparer 添加代码,但这是我可以接受的。
    • IQueryModelGenerator 在 EF Core 3.1 中似乎不再存在。有没有人有一个很好的通用替代方案来连接查询管道?
    • @RyanThomas 帖子已更新。还有一个类似的工厂/处理器对用于后处理。
    猜你喜欢
    • 2020-06-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-21
    相关资源
    最近更新 更多