Radiofisik

my knowledge base

EF inside

Как транслируется запрос

Для начала представим ef выполняет простейшую задачу

 var context = new BloggingContext();
 var blogQuuery = context.Blogs.Where(blog => blog.Url == "testUrl");
 var blog = await blogQuuery.FirstOrDefaultAsync();

Вся магия происходит при выполнении FirstOrDefaultAsync

public static Task<TSource> FirstOrDefaultAsync<TSource>(
            [NotNull] this IQueryable<TSource> source,
            CancellationToken cancellationToken = default)
        {
            Check.NotNull(source, nameof(source));

            return ExecuteAsync<TSource, Task<TSource>>(QueryableMethods.FirstOrDefaultWithoutPredicate, source, cancellationToken);
        }

который внутри вызывает ExecuteAsync который через еще один вызов переходит в efcore\src\EFCore\Extensions\EntityFrameworkQueryableExtensions.cs

private static TResult ExecuteAsync<TSource, TResult>(
    MethodInfo operatorMethodInfo,
    IQueryable<TSource> source,
    Expression expression,
    CancellationToken cancellationToken = default)
{
    if (source.Provider is IAsyncQueryProvider provider)
    {
        if (operatorMethodInfo.IsGenericMethod)
        {
            operatorMethodInfo
                = operatorMethodInfo.GetGenericArguments().Length == 2
                ? operatorMethodInfo.MakeGenericMethod(typeof(TSource), typeof(TResult).GetGenericArguments().Single())
                : operatorMethodInfo.MakeGenericMethod(typeof(TSource));
        }

        return provider.ExecuteAsync<TResult>(
            Expression.Call(
                instance: null,
                method: operatorMethodInfo,
                arguments: expression == null
                ? new[] { source.Expression }
                : new[] { source.Expression, expression }),
            cancellationToken);
    }

    throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync);
}

который через несколько промежуточных вызовов вызывает

efcore\src\EFCore\Query\Internal\QueryCompiler.cs

public virtual TResult ExecuteAsync<TResult>(Expression query, CancellationToken cancellationToken = default)
        {
            Check.NotNull(query, nameof(query));

            var queryContext = _queryContextFactory.Create();

            queryContext.CancellationToken = cancellationToken;

            query = ExtractParameters(query, queryContext, _logger);

            var compiledQuery
                = _compiledQueryCache
                    .GetOrAddQuery(
                        _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, async: true),
                        () => CompileQueryCore<TResult>(_database, query, _model, true));

            return compiledQuery(queryContext);
        }

который использует ` QueryCompilationContext` для генерации QueryExecutor

 public virtual Func<QueryContext, TResult> CreateQueryExecutor<TResult>([NotNull] Expression query)
        {
            Check.NotNull(query, nameof(query));

            Logger.QueryCompilationStarting(_expressionPrinter, query);

            query = _queryTranslationPreprocessorFactory.Create(this).Process(query);
            // Convert EntityQueryable to ShapedQueryExpression
     // в следующем разделе будем инжектить свой визитор 
            query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Visit(query);
            query = _queryTranslationPostprocessorFactory.Create(this).Process(query);

            // Inject actual entity materializer
            // Inject tracking
            query = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(query);

            // If any additional parameters were added during the compilation phase (e.g. entity equality ID expression),
            // wrap the query with code adding those parameters to the query context
            query = InsertRuntimeParameters(query);

            var queryExecutorExpression = Expression.Lambda<Func<QueryContext, TResult>>(
                query,
                QueryContextParameter);

            try
            {
                return queryExecutorExpression.Compile();
            }
            finally
            {
                Logger.QueryExecutionPlanned(_expressionPrinter, queryExecutorExpression);
            }
        }

Модификация трансляции функций

теперь представим нам понадобилась функция для выполнения подзапросов. Создадим метод расширения, который будем использовать для трансляции

public static class SubQueryQueryableExtensions
{
    private static readonly MethodInfo AsSubQueryMethodInfo = typeof(SubQueryQueryableExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static).Single(m => m.Name == nameof(AsSubQuery) && m.IsGenericMethod);

    public static IQueryable<TEntity> AsSubQuery<TEntity>(this IQueryable<TEntity> source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));

        return source.Provider.CreateQuery<TEntity>(Expression.Call(null, AsSubQueryMethodInfo.MakeGenericMethod(typeof(TEntity)), source.Expression));
    }
}

Для обработки трансляции расширим RelationalQueryableMethodTranslatingExpressionVisitor

    public class SubQueryMethodTranslatingExpressionVisitor: RelationalQueryableMethodTranslatingExpressionVisitor
    {
        private readonly IRelationalTypeMappingSource _typeMappingSource;

        /// <inheritdoc />
        public SubQueryMethodTranslatingExpressionVisitor(
            QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
            RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies,
            QueryCompilationContext queryCompilationContext,
            IRelationalTypeMappingSource typeMappingSource)
            : base(dependencies, relationalDependencies, queryCompilationContext)
        {
            _typeMappingSource = typeMappingSource ?? throw new ArgumentNullException(nameof(typeMappingSource));
        }

        /// <inheritdoc />
        protected SubQueryMethodTranslatingExpressionVisitor(
            SubQueryMethodTranslatingExpressionVisitor parentVisitor,
            IRelationalTypeMappingSource typeMappingSource)
            : base(parentVisitor)
        {
            _typeMappingSource = typeMappingSource ?? throw new ArgumentNullException(nameof(typeMappingSource));
        }

        /// <inheritdoc />
        protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor()
        {
            return new SubQueryMethodTranslatingExpressionVisitor(this, _typeMappingSource);
        }

        /// <inheritdoc />
        protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
        {
            return this.TranslateRelationalMethods(methodCallExpression, QueryCompilationContext) ??
                   base.VisitMethodCall(methodCallExpression);
        }
        
        public Expression? TranslateRelationalMethods(MethodCallExpression methodCallExpression, QueryCompilationContext queryCompilationContext)
        {
            if (methodCallExpression == null)
                throw new ArgumentNullException(nameof(methodCallExpression));

            if (methodCallExpression.Method.DeclaringType == typeof(SubQueryQueryableExtensions))
            {
                if (methodCallExpression.Method.Name == nameof(SubQueryQueryableExtensions.AsSubQuery))
                {
                    var expression = this.Visit(methodCallExpression.Arguments[0]);

                    if (expression is ShapedQueryExpression shapedQueryExpression)
                    {
                        ((SelectExpression)shapedQueryExpression.QueryExpression).PushdownIntoSubquery();
                        return shapedQueryExpression;
                    }
                }
            }

            return null;
        }
    }

Но для того чтобы он был задействован вместо оригинального надо использовать фабрику

    public class SubQueryMethodTranslatingExpressionVisitorFactory: IQueryableMethodTranslatingExpressionVisitorFactory
    {
        private readonly QueryableMethodTranslatingExpressionVisitorDependencies _dependencies;
        private readonly RelationalQueryableMethodTranslatingExpressionVisitorDependencies _relationalDependencies;
        private readonly IRelationalTypeMappingSource _typeMappingSource;

        /// <summary>
        /// Initializes new instance of <see cref="SubQueryMethodTranslatingExpressionVisitorFactory"/>.
        /// </summary>
        /// <param name="dependencies">Dependencies.</param>
        /// <param name="relationalDependencies">Relational dependencies.</param>
        /// <param name="typeMappingSource">Type mapping source.</param>
        public SubQueryMethodTranslatingExpressionVisitorFactory(
            QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
            RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies,
            IRelationalTypeMappingSource typeMappingSource)
        {
            _dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies));
            _relationalDependencies = relationalDependencies ?? throw new ArgumentNullException(nameof(relationalDependencies));
            _typeMappingSource = typeMappingSource ?? throw new ArgumentNullException(nameof(typeMappingSource));
        }

        /// <inheritdoc />
        public QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext)
        {
            return new SubQueryMethodTranslatingExpressionVisitor(_dependencies, _relationalDependencies, queryCompilationContext, _typeMappingSource);
        }
    }

чтобы внедрить которую надо создать расширение

public class SubQueryDbContextOptionsExtension: IDbContextOptionsExtension
{
    public void ApplyServices(IServiceCollection services)
    {
        services.AddSingleton<IQueryableMethodTranslatingExpressionVisitorFactory, SubQueryMethodTranslatingExpressionVisitorFactory>();
    }

    public void Validate(IDbContextOptions options)
    {
    }

    public DbContextOptionsExtensionInfo Info { get; }

    public SubQueryDbContextOptionsExtension()
    {
        Info = new SubQueryDbContextOptionsExtensionInfo(this);
    }

    private class SubQueryDbContextOptionsExtensionInfo : DbContextOptionsExtensionInfo
    {
        private readonly SubQueryDbContextOptionsExtension _extension;

        public override long GetServiceProviderHashCode()
        {
            return 0;
        }

        public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
        {
        }

        public override bool IsDatabaseProvider => false;
        public override string LogFragment { get; }


        public SubQueryDbContextOptionsExtensionInfo(SubQueryDbContextOptionsExtension extension)
            : base(extension)
            {
                _extension = extension ?? throw new ArgumentNullException(nameof(extension));
            }
    }
}

А чтобы удобно его подключить метод расширения

public static class SubQueryDbContextOptionsBuilderExtensions
{
    public static DbContextOptionsBuilder AddSubQuerySupport(this DbContextOptionsBuilder optionsBuilder)
    {
        var infrastructure = (IDbContextOptionsBuilderInfrastructure) optionsBuilder;
        var extension = optionsBuilder.Options.FindExtension<SubQueryDbContextOptionsExtension>() ?? new SubQueryDbContextOptionsExtension();
        infrastructure.AddOrUpdateExtension(extension);

        return optionsBuilder;
    }
}

и подключить к контексту

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseNpgsql("Host=127.0.0.1;Port=55432;Database=testdb;Username=postgres;Password=postgres");
    optionsBuilder.AddSubQuerySupport();
}

в результате получим возможность выполнять подзапросы, так например код

var context = new BloggingContext();
var query = context.Blogs
	.Where(b => b.Url == "")
	.AsSubQuery()
	.Select(b => new{b.BlogId});
var result = query.FirstOrDefault();

генерирует запрос

SELECT t."BlogId"
FROM (
    SELECT b."BlogId"
    FROM "Blogs" AS b
    WHERE b."Url" = ''
) AS t

получившийся код https://github.com/Radiofisik/EFTest в ветке efinside

Использованные материалы