diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
index 43a324bcb12..92c5d259842 100644
--- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Data;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore.Query.Internal;
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
@@ -292,6 +294,102 @@ public static int ExecuteSqlRaw(
}
}
+ ///
+ /// Creates a LINQ query based on a raw SQL query, which returns a result set of a scalar type natively supported by the database
+ /// provider.
+ ///
+ ///
+ ///
+ /// To use this method with a return type that isn't natively supported by the database provider, use the
+ ///
+ /// method.
+ ///
+ ///
+ /// The returned can be composed over using LINQ to build more complex queries.
+ ///
+ ///
+ /// Note that this method does not start a transaction. To use this method with a transaction, first call
+ /// or .
+ ///
+ ///
+ /// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
+ /// attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
+ /// arguments. Any parameter values you supply will automatically be converted to a DbParameter.
+ ///
+ ///
+ /// However, never pass a concatenated or interpolated string ($"") with non-validated user-provided values
+ /// into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
+ /// consider using to create parameters.
+ ///
+ ///
+ /// See Executing raw SQL commands with EF Core
+ /// for more information and examples.
+ ///
+ ///
+ /// The for the context.
+ /// The raw SQL query.
+ /// The values to be assigned to parameters.
+ /// An representing the raw SQL query.
+ [StringFormatMethod("sql")]
+ public static IQueryable SqlQueryRaw(
+ this DatabaseFacade databaseFacade,
+ [NotParameterized] string sql,
+ params object[] parameters)
+ {
+ Check.NotNull(sql, nameof(sql));
+ Check.NotNull(parameters, nameof(parameters));
+
+ var facadeDependencies = GetFacadeDependencies(databaseFacade);
+
+ return facadeDependencies.QueryProvider
+ .CreateQuery(new SqlQueryRootExpression(
+ facadeDependencies.QueryProvider, typeof(TResult), sql, Expression.Constant(parameters)));
+ }
+
+ ///
+ /// Creates a LINQ query based on a raw SQL query, which returns a result set of a scalar type natively supported by the database
+ /// provider.
+ ///
+ ///
+ ///
+ /// To use this method with a return type that isn't natively supported by the database provider, use the
+ ///
+ /// method.
+ ///
+ ///
+ /// The returned can be composed over using LINQ to build more complex queries.
+ ///
+ ///
+ /// Note that this method does not start a transaction. To use this method with a transaction, first call
+ /// or .
+ ///
+ ///
+ /// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
+ /// attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
+ /// arguments. Any parameter values you supply will automatically be converted to a DbParameter.
+ ///
+ ///
+ /// See Executing raw SQL commands with EF Core
+ /// for more information and examples.
+ ///
+ ///
+ /// The for the context.
+ /// The interpolated string representing a SQL query with parameters.
+ /// An representing the interpolated string SQL query.
+ public static IQueryable SqlQuery(
+ this DatabaseFacade databaseFacade,
+ [NotParameterized] FormattableString sql)
+ {
+ Check.NotNull(sql, nameof(sql));
+ Check.NotNull(sql.Format, nameof(sql.Format));
+
+ var facadeDependencies = GetFacadeDependencies(databaseFacade);
+
+ return facadeDependencies.QueryProvider
+ .CreateQuery(new SqlQueryRootExpression(
+ facadeDependencies.QueryProvider, typeof(TResult), sql.Format, Expression.Constant(sql.GetArguments())));
+ }
+
///
/// Executes the given SQL against the database and returns the number of rows affected.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 17c2c2e19e9..2c4c843fed8 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -764,7 +764,7 @@ public static string FromSqlMissingColumn(object? column)
column);
///
- /// 'FromSqlRaw' or 'FromSqlInterpolated' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.
+ /// 'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.
///
public static string FromSqlNonComposable
=> GetString("FromSqlNonComposable");
@@ -1425,6 +1425,14 @@ public static string SqlQueryOverrideMismatch(object? propertySpecification, obj
GetString("SqlQueryOverrideMismatch", nameof(propertySpecification), nameof(query)),
propertySpecification, query);
+ ///
+ /// The element type '{elementType}' used in 'SqlQuery' method is not natively supported by your database provider. Either use a supported element type, or use ModelConfigurationBuilder.DefaultTypeMapping to define a mapping for your type.
+ ///
+ public static string SqlQueryUnmappedType(object? elementType)
+ => string.Format(
+ GetString("SqlQueryUnmappedType", nameof(elementType)),
+ elementType);
+
///
/// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index c79b398c978..70af619f01d 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -401,7 +401,7 @@
The required column '{column}' was not present in the results of a 'FromSql' operation.
- 'FromSqlRaw' or 'FromSqlInterpolated' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.
+ 'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.The property '{propertySpecification}' has specific configuration for the function '{function}', but it isn't mapped to a column on that function return. Remove the specific configuration, or map an entity type that contains this property to '{function}'.
@@ -951,6 +951,9 @@
The property '{propertySpecification}' has specific configuration for the SQL query '{query}', but isn't mapped to a column on that query. Remove the specific configuration, or map an entity type that contains this property to '{query}'.
+
+ The element type '{elementType}' used in 'SqlQuery' method is not natively supported by your database provider. Either use a supported element type, or use ModelConfigurationBuilder.DefaultTypeMapping to define a mapping for your type.
+
The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter.
diff --git a/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs b/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
index 3956a68acef..6ebad76598e 100644
--- a/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
+++ b/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
@@ -1264,7 +1264,12 @@ private void InitializeFields()
if (!readerColumns.TryGetValue(column.Name!, out var ordinal))
{
- throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name));
+ if (_columns.Count != 1)
+ {
+ throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name));
+ }
+
+ ordinal = 0;
}
newColumnMap[ordinal] = column;
diff --git a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs
index b73fa6e0e9d..817b2614048 100644
--- a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs
+++ b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs
@@ -134,7 +134,12 @@ public static int[] BuildIndexMap(IReadOnlyList columnNames, DbDataReade
var columnName = columnNames[i];
if (!readerColumns.TryGetValue(columnName, out var ordinal))
{
- throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName));
+ if (columnNames.Count != 1)
+ {
+ throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName));
+ }
+
+ ordinal = 0;
}
indexMap[i] = ordinal;
diff --git a/src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs b/src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs
new file mode 100644
index 00000000000..3a9bed45954
--- /dev/null
+++ b/src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public sealed class SqlQueryRootExpression : QueryRootExpression
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlQueryRootExpression(
+ IAsyncQueryProvider queryProvider,
+ Type elementType,
+ string sql,
+ Expression argument)
+ : base(queryProvider, elementType)
+ {
+ Sql = sql;
+ Argument = argument;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlQueryRootExpression(
+ Type elementType,
+ string sql,
+ Expression argument)
+ : base(elementType)
+ {
+ Sql = sql;
+ Argument = argument;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public string Sql { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public Expression Argument { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override Expression DetachQueryProvider()
+ => new SqlQueryRootExpression(ElementType, Sql, Argument);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var argument = visitor.Visit(Argument);
+
+ return argument != Argument
+ ? new SqlQueryRootExpression(ElementType, Sql, argument)
+ : this;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append($"SqlQuery<{ElementType.ShortDisplayName()}>({Sql}, ");
+ expressionPrinter.Visit(Argument);
+ expressionPrinter.AppendLine(")");
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override bool Equals(object? obj)
+ => obj != null
+ && (ReferenceEquals(this, obj)
+ || obj is SqlQueryRootExpression sqlQueryRootExpression
+ && Equals(sqlQueryRootExpression));
+
+ private bool Equals(SqlQueryRootExpression sqlQueryRootExpression)
+ => base.Equals(sqlQueryRootExpression)
+ && Sql == sqlQueryRootExpression.Sql
+ && ExpressionEqualityComparer.Instance.Equals(Argument, sqlQueryRootExpression.Argument);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(base.GetHashCode(), Sql, ExpressionEqualityComparer.Instance.GetHashCode(Argument));
+}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index e41ba43fc6e..3f7486a49d6 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -151,6 +152,30 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
new QueryExpressionReplacingExpressionVisitor(shapedQueryExpression.QueryExpression, clonedSelectExpression)
.Visit(shapedQueryExpression.ShaperExpression));
+ case SqlQueryRootExpression sqlQueryRootExpression:
+ var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType);
+ if (typeMapping == null)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.SqlQueryUnmappedType(sqlQueryRootExpression.ElementType.DisplayName()));
+ }
+
+ var selectExpression = new SelectExpression(sqlQueryRootExpression.Type, typeMapping,
+ new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument));
+
+ Expression shaperExpression = new ProjectionBindingExpression(
+ selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable());
+
+ if (sqlQueryRootExpression.ElementType != shaperExpression.Type)
+ {
+ Check.DebugAssert(sqlQueryRootExpression.ElementType.MakeNullable() == shaperExpression.Type,
+ "expression.Type must be nullable of targetType");
+
+ shaperExpression = Expression.Convert(shaperExpression, sqlQueryRootExpression.ElementType);
+ }
+
+ return new ShapedQueryExpression(selectExpression, shaperExpression);
+
default:
return base.VisitExtension(extensionExpression);
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs
index 9fb23b65d9b..2f0679525a2 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs
@@ -47,10 +47,12 @@ public sealed record RelationalQueryableMethodTranslatingExpressionVisitorDepend
[EntityFrameworkInternal]
public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
IRelationalSqlTranslatingExpressionVisitorFactory relationalSqlTranslatingExpressionVisitorFactory,
- ISqlExpressionFactory sqlExpressionFactory)
+ ISqlExpressionFactory sqlExpressionFactory,
+ IRelationalTypeMappingSource typeMappingSource)
{
RelationalSqlTranslatingExpressionVisitorFactory = relationalSqlTranslatingExpressionVisitorFactory;
SqlExpressionFactory = sqlExpressionFactory;
+ TypeMappingSource = typeMappingSource;
}
///
@@ -62,4 +64,9 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
/// The SQL expression factory.
///
public ISqlExpressionFactory SqlExpressionFactory { get; init; }
+
+ ///
+ /// The relational type mapping souce.
+ ///
+ public IRelationalTypeMappingSource TypeMappingSource { get; init; }
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
index 20690f046a0..2ec6dad9698 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
@@ -16,8 +16,6 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
///
public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBase
{
- private readonly ITableBase _table;
-
///
/// Creates a new instance of the class.
///
@@ -39,15 +37,26 @@ public FromSqlExpression(ITableBase defaultTableBase, string sql, Expression arg
//{
//}
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// An alias to use for this table source.
+ /// A user-provided custom SQL for the table source.
+ /// A user-provided parameters to pass to the custom SQL.
+ public FromSqlExpression(string alias, string sql, Expression arguments)
+ : this(alias, null, sql, arguments, annotations: null)
+ {
+ }
+
private FromSqlExpression(
string alias,
- ITableBase tableBase,
+ ITableBase? tableBase,
string sql,
Expression arguments,
IEnumerable? annotations)
: base(alias, annotations)
{
- _table = tableBase;
+ Table = tableBase;
Sql = sql;
Arguments = arguments;
}
@@ -75,7 +84,7 @@ public override string? Alias
///
/// The associated with given table source if any, otherwise.
///
- public virtual ITableBase? Table => _table;
+ public virtual ITableBase? Table { get; }
///
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
@@ -85,12 +94,12 @@ public override string? Alias
/// This expression if no children changed, or an expression with the updated children.
public virtual FromSqlExpression Update(Expression arguments)
=> arguments != Arguments
- ? new FromSqlExpression(Alias, _table, Sql, arguments, GetAnnotations())
+ ? new FromSqlExpression(Alias, Table, Sql, arguments, GetAnnotations())
: this;
///
protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations)
- => new FromSqlExpression(Alias, _table, Sql, Arguments, annotations);
+ => new FromSqlExpression(Alias, Table, Sql, Arguments, annotations);
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
@@ -98,7 +107,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
///
public virtual TableExpressionBase Clone()
- => new FromSqlExpression(Alias, _table, Sql, Arguments, GetAnnotations());
+ => new FromSqlExpression(Alias, Table, Sql, Arguments, GetAnnotations());
///
protected override void Print(ExpressionPrinter expressionPrinter)
@@ -116,11 +125,11 @@ public override bool Equals(object? obj)
private bool Equals(FromSqlExpression fromSqlExpression)
=> base.Equals(fromSqlExpression)
- && _table == fromSqlExpression._table
+ && Table == fromSqlExpression.Table
&& Sql == fromSqlExpression.Sql
&& ExpressionEqualityComparer.Instance.Equals(Arguments, fromSqlExpression.Arguments);
///
public override int GetHashCode()
- => HashCode.Combine(base.GetHashCode(), Sql);
+ => HashCode.Combine(base.GetHashCode(), Table, Sql, Arguments);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index bf4aa8e88a9..cc07e0c98df 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -24,6 +24,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
public sealed partial class SelectExpression : TableExpressionBase
{
private const string DiscriminatorColumnAlias = "Discriminator";
+ private const string SqlQuerySingleColumnAlias = "Value";
private static readonly IdentifierComparer IdentifierComparerInstance = new();
private static readonly Dictionary MirroredOperationMap =
@@ -102,6 +103,18 @@ internal SelectExpression(SqlExpression? projection)
}
}
+ internal SelectExpression(Type type, RelationalTypeMapping typeMapping, FromSqlExpression fromSqlExpression)
+ : base(null)
+ {
+ var tableReferenceExpression = new TableReferenceExpression(this, fromSqlExpression.Alias!);
+ AddTable(fromSqlExpression, tableReferenceExpression);
+
+ var columnExpression = new ConcreteColumnExpression(
+ SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType());
+
+ _projectionMapping[new ProjectionMember()] = columnExpression;
+ }
+
internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpressionFactory)
: base(null)
{
@@ -3211,7 +3224,7 @@ public bool IsNonComposedFromSql()
pe => pe.Expression is ColumnExpression column
&& string.Equals(fromSql.Alias, column.TableAlias, StringComparison.OrdinalIgnoreCase))
&& _projectionMapping.TryGetValue(new ProjectionMember(), out var mapping)
- && mapping.Type == typeof(Dictionary);
+ && mapping.Type == (fromSql.Table == null ? typeof(int) : typeof(Dictionary));
///
/// Prepares the to apply aggregate operation over it.
diff --git a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs
index bf3db896b66..b24964ee677 100644
--- a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs
+++ b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs
@@ -27,7 +27,8 @@ public RelationalDatabaseFacadeDependencies(
IConcurrencyDetector concurrencyDetector,
IRelationalConnection relationalConnection,
IRawSqlCommandBuilder rawSqlCommandBuilder,
- ICoreSingletonOptions coreOptions)
+ ICoreSingletonOptions coreOptions,
+ IAsyncQueryProvider queryProvider)
{
TransactionManager = transactionManager;
DatabaseCreator = databaseCreator;
@@ -39,6 +40,7 @@ public RelationalDatabaseFacadeDependencies(
RelationalConnection = relationalConnection;
RawSqlCommandBuilder = rawSqlCommandBuilder;
CoreOptions = coreOptions;
+ QueryProvider = queryProvider;
}
///
@@ -123,4 +125,12 @@ public RelationalDatabaseFacadeDependencies(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual ICoreSingletonOptions CoreOptions { get; init; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IAsyncQueryProvider QueryProvider { get; init; }
}
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
index bcf4e7118c7..8d0d177cec5 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
@@ -176,6 +176,12 @@ protected override Expression VisitExtension(Expression extensionExpression)
return ApplyQueryFilter(entityType, navigationExpansionExpression);
+ case QueryRootExpression queryRootExpression:
+ var currentTree = new NavigationTreeExpression(Expression.Default(queryRootExpression.ElementType));
+ var parameterName = GetParameterName("e");
+
+ return new NavigationExpansionExpression(queryRootExpression, currentTree, currentTree, parameterName);
+
case NavigationExpansionExpression:
case OwnedNavigationReference:
return extensionExpression;
diff --git a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs
index c9fe80f5866..a29318d7bd9 100644
--- a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs
+++ b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs
@@ -64,5 +64,10 @@ public interface IDatabaseFacadeDependencies
///
/// The core options.
///
- public ICoreSingletonOptions CoreOptions { get; }
+ ICoreSingletonOptions CoreOptions { get; }
+
+ ///
+ /// The async query provider.
+ ///
+ IAsyncQueryProvider QueryProvider { get; }
}
diff --git a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs
index 2f5b9294afa..f59c8e84489 100644
--- a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs
+++ b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs
@@ -25,7 +25,8 @@ public DatabaseFacadeDependencies(
IEnumerable databaseProviders,
IDiagnosticsLogger commandLogger,
IConcurrencyDetector concurrencyDetector,
- ICoreSingletonOptions coreOptions)
+ ICoreSingletonOptions coreOptions,
+ IAsyncQueryProvider queryProvider)
{
TransactionManager = transactionManager;
DatabaseCreator = databaseCreator;
@@ -35,6 +36,7 @@ public DatabaseFacadeDependencies(
CommandLogger = commandLogger;
ConcurrencyDetector = concurrencyDetector;
CoreOptions = coreOptions;
+ QueryProvider = queryProvider;
}
///
@@ -100,4 +102,12 @@ public DatabaseFacadeDependencies(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual ICoreSingletonOptions CoreOptions { get; init; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IAsyncQueryProvider QueryProvider { get; init; }
}
diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSqlQueryTestBase.cs
new file mode 100644
index 00000000000..a195aa6d84b
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSqlQueryTestBase.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.TestModels.Northwind;
+
+// ReSharper disable FormatStringProblem
+// ReSharper disable InconsistentNaming
+// ReSharper disable ConvertToConstant.Local
+// ReSharper disable AccessToDisposedClosure
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public abstract class NorthwindSqlQueryTestBase : IClassFixture
+ where TFixture : NorthwindQueryRelationalFixture, new()
+{
+ protected NorthwindSqlQueryTestBase(TFixture fixture)
+ {
+ Fixture = fixture;
+ Fixture.TestSqlLoggerFactory.Clear();
+ }
+ protected TFixture Fixture { get; }
+
+ public static IEnumerable