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 299db58035f..895a6e2e8e9 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 a40b3ce2ca1..6a0c4368445 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 IsAsyncData = new[] { new object[] { false }, new object[] { true } }; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_over_int(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw(NormalizeDelimitersInRawString(@"SELECT [ProductID] FROM [Products]")); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(77, result.Count); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_composed_Contains(bool async) + { + using var context = CreateContext(); + var query = context.Set() + .Where(e => context.Database + .SqlQuery(NormalizeDelimitersInInterpolatedString(@$"SELECT [ProductID] AS [Value] FROM [Products]")) + .Contains(e.OrderID)); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(0, result.Count); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_composed_Join(bool async) + { + using var context = CreateContext(); + var query = from o in context.Set() + join p in context.Database.SqlQuery(NormalizeDelimitersInInterpolatedString(@$"SELECT [ProductID] AS [Value] FROM [Products]")) + on o.OrderID equals p + select new { o, p }; + + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(0, result.Count); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_over_int_with_parameter(bool async) + { + using var context = CreateContext(); + var value = 10; + var query = context.Database.SqlQuery(NormalizeDelimitersInInterpolatedString(@$"SELECT [ProductID] FROM [Products] WHERE [ProductID] = {value}")); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(10, result.Single()); + } + + protected string NormalizeDelimitersInRawString(string sql) + => Fixture.TestStore.NormalizeDelimitersInRawString(sql); + + protected FormattableString NormalizeDelimitersInInterpolatedString(FormattableString sql) + => Fixture.TestStore.NormalizeDelimitersInInterpolatedString(sql); + + protected abstract DbParameter CreateDbParameter(string name, object value); + + protected NorthwindContext CreateContext() + => Fixture.CreateContext(); +} diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs index 2fcd805354e..c322ffd67f5 100644 --- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs @@ -799,11 +799,28 @@ public class Layout public int Height { get; set; } } + public class HolderClass + { + public int Id { get; set; } + public HoldingEnum HoldingEnum { get; set; } + } + + public enum HoldingEnum + { + Value1, + Value2 + } + public abstract class CustomConvertersFixtureBase : BuiltInDataTypesFixtureBase { protected override string StoreName => "CustomConverters"; + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.DefaultTypeMapping().HasConversion(); + } + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); @@ -1319,6 +1336,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con (v1, v2) => v1.SequenceEqual(v2), v => v.GetHashCode(), v => new List(v))); + + modelBuilder.Entity().HasData(new HolderClass { Id = 1, HoldingEnum = HoldingEnum.Value2 }); } private static class StringToDictionarySerializer diff --git a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs index 4493b30ed04..d6c75cdba05 100644 --- a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs @@ -160,6 +160,8 @@ public virtual void Columns_have_expected_data_types() EmailTemplate.TemplateType ---> [int] [Precision = 10 Scale = 0] EntityWithValueWrapper.Id ---> [int] [Precision = 10 Scale = 0] EntityWithValueWrapper.Wrapper ---> [nullable nvarchar] [MaxLength = -1] +HolderClass.HoldingEnum ---> [int] [Precision = 10 Scale = 0] +HolderClass.Id ---> [int] [Precision = 10 Scale = 0] Load.Fuel ---> [float] [Precision = 53] Load.LoadId ---> [int] [Precision = 10 Scale = 0] MaxLengthDataTypes.ByteArray5 ---> [nullable varbinary] [MaxLength = 7] @@ -295,6 +297,24 @@ public override void Value_conversion_on_enum_collection_contains() CoreStrings.TranslationFailed("")[47..], Assert.Throws(() => base.Value_conversion_on_enum_collection_contains()).Message); + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task SqlQuery_with_converted_type_using_model_configuration_builder_works(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw("SELECT [HoldingEnum] FROM [HolderClass]"); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(HoldingEnum.Value2, result.Single()); + + AssertSql( + @"SELECT [HoldingEnum] FROM [HolderClass]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSqlQuerySqlServerTest.cs new file mode 100644 index 00000000000..f23f53bbc43 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSqlQuerySqlServerTest.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlClient; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSqlQuerySqlServerTest : NorthwindSqlQueryTestBase> +{ + public NorthwindSqlQuerySqlServerTest(NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task SqlQueryRaw_over_int(bool async) + { + await base.SqlQueryRaw_over_int(async); + + AssertSql( + @"SELECT ""ProductID"" FROM ""Products"""); + } + + public override async Task SqlQuery_composed_Contains(bool async) + { + await base.SqlQuery_composed_Contains(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Orders] AS [o] +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT ""ProductID"" AS ""Value"" FROM ""Products"" + ) AS [t] + WHERE CAST([t].[Value] AS int) = [o].[OrderID])"); + } + + public override async Task SqlQuery_composed_Join(bool async) + { + await base.SqlQuery_composed_Join(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], CAST([t].[Value] AS int) AS [p] +FROM [Orders] AS [o] +INNER JOIN ( + SELECT ""ProductID"" AS ""Value"" FROM ""Products"" +) AS [t] ON [o].[OrderID] = CAST([t].[Value] AS int)"); + } + + public override async Task SqlQuery_over_int_with_parameter(bool async) + { + await base.SqlQuery_over_int_with_parameter(async); + + AssertSql( + @"p0='10' + +SELECT ""ProductID"" FROM ""Products"" WHERE ""ProductID"" = @p0"); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new SqlParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index 0185d15146a..3946fac4630 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -1,8 +1,6 @@ // 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; - namespace Microsoft.EntityFrameworkCore.Query; public class NorthwindFunctionsQuerySqliteTest : NorthwindFunctionsQueryRelationalTestBase< diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSqlQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSqlQuerySqliteTest.cs new file mode 100644 index 00000000000..d54eb4c869f --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSqlQuerySqliteTest.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.Sqlite; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSqlQuerySqliteTest : NorthwindSqlQueryTestBase> +{ + public NorthwindSqlQuerySqliteTest(NorthwindQuerySqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new SqliteParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}