Для начала представим 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
Использованные материалы