diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
index 3bfe32f3bab..cfbc0526d29 100644
--- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Data;
+using Microsoft.EntityFrameworkCore.Query.Internal;
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
@@ -260,6 +261,27 @@ public static int ExecuteSqlRaw(
}
}
+ ///
+ /// .
+ ///
+ /// .
+ /// .
+ /// .
+ /// .
+ public static IQueryable SqlQuery(
+ 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(typeof(TResult), sql, Expression.Constant(parameters)));
+ }
+
///
/// 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 662861998b6..253479b2d92 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -771,12 +771,6 @@ public static string FromSqlMissingColumn(object? column)
GetString("FromSqlMissingColumn", nameof(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.
- ///
- public static string FromSqlNonComposable
- => GetString("FromSqlNonComposable");
-
///
/// 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}'.
///
@@ -895,14 +889,6 @@ public static string InvalidDerivedTypeInEntityProjection(object? derivedType, o
GetString("InvalidDerivedTypeInEntityProjection", nameof(derivedType), nameof(entityType)),
derivedType, entityType);
- ///
- /// A FromSqlExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'.
- ///
- public static string InvalidFromSqlArguments(object? expressionType, object? valueType)
- => string.Format(
- GetString("InvalidFromSqlArguments", nameof(expressionType), nameof(valueType)),
- expressionType, valueType);
-
///
/// The grouping key '{keySelector}' is of type '{keyType}' which is not valid key.
///
@@ -967,6 +953,114 @@ public static string InvalidMinBatchSize(object? value)
GetString("InvalidMinBatchSize", nameof(value)),
value);
+ ///
+ /// A RawSqlQuerylExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'.
+ ///
+ public static string InvalidRawSqlQueryArguments(object? expressionType, object? valueType)
+ => string.Format(
+ GetString("InvalidRawSqlQueryArguments", nameof(expressionType), nameof(valueType)),
+ expressionType, valueType);
+
+ ///
+ /// Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner.
+ ///
+ public static string JsonEntityMappedToDifferentViewThanOwner(object? jsonType, object? viewName, object? ownerType, object? ownerViewName)
+ => string.Format(
+ GetString("JsonEntityMappedToDifferentViewThanOwner", nameof(jsonType), nameof(viewName), nameof(ownerType), nameof(ownerViewName)),
+ jsonType, viewName, ownerType, ownerViewName);
+
+ ///
+ /// Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column.
+ ///
+ public static string JsonEntityMultipleRootsMappedToTheSameJsonColumn(object? column, object? table)
+ => string.Format(
+ GetString("JsonEntityMultipleRootsMappedToTheSameJsonColumn", nameof(column), nameof(table)),
+ column, table);
+
+ ///
+ /// Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves.
+ ///
+ public static string JsonEntityOwnedByNonJsonOwnedType(object? nonJsonType, object? table)
+ => string.Format(
+ GetString("JsonEntityOwnedByNonJsonOwnedType", nameof(nonJsonType), nameof(table)),
+ nonJsonType, table);
+
+ ///
+ /// Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner.
+ ///
+ public static string JsonEntityReferencingRegularEntity(object? jsonEntity)
+ => string.Format(
+ GetString("JsonEntityReferencingRegularEntity", nameof(jsonEntity)),
+ jsonEntity);
+
+ ///
+ /// Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'.
+ ///
+ public static string JsonEntityWithDefaultValueSetOnItsProperty(object? jsonEntity, object? property)
+ => string.Format(
+ GetString("JsonEntityWithDefaultValueSetOnItsProperty", nameof(jsonEntity), nameof(property)),
+ jsonEntity, property);
+
+ ///
+ /// Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly.
+ ///
+ public static string JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey(object? keyProperty, object? jsonEntity)
+ => string.Format(
+ GetString("JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey", nameof(keyProperty), nameof(jsonEntity)),
+ keyProperty, jsonEntity);
+
+ ///
+ /// Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported.
+ ///
+ public static string JsonEntityWithExplicitlyConfiguredOrdinalKey(object? jsonEntity)
+ => string.Format(
+ GetString("JsonEntityWithExplicitlyConfiguredOrdinalKey", nameof(jsonEntity)),
+ jsonEntity);
+
+ ///
+ /// Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}.
+ ///
+ public static string JsonEntityWithIncorrectNumberOfKeyProperties(object? jsonEntity, object? expectedCount, object? actualCount)
+ => string.Format(
+ GetString("JsonEntityWithIncorrectNumberOfKeyProperties", nameof(jsonEntity), nameof(expectedCount), nameof(actualCount)),
+ jsonEntity, expectedCount, actualCount);
+
+ ///
+ /// Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property.
+ ///
+ public static string JsonEntityWithMultiplePropertiesMappedToSameJsonProperty(object? jsonEntity, object? property)
+ => string.Format(
+ GetString("JsonEntityWithMultiplePropertiesMappedToSameJsonProperty", nameof(jsonEntity), nameof(property)),
+ jsonEntity, property);
+
+ ///
+ /// Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities.
+ ///
+ public static string JsonEntityWithNonTphInheritanceOnOwner(object? rootType, object? tph)
+ => string.Format(
+ GetString("JsonEntityWithNonTphInheritanceOnOwner", nameof(rootType), nameof(tph)),
+ rootType, tph);
+
+ ///
+ /// Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported.
+ ///
+ public static string JsonEntityWithOwnerNotMappedToTableOrView(object? entity)
+ => string.Format(
+ GetString("JsonEntityWithOwnerNotMappedToTableOrView", nameof(entity)),
+ entity);
+
+ ///
+ /// Table splitting is not supported for entities containing entities mapped to JSON.
+ ///
+ public static string JsonEntityWithTableSplittingIsNotSupported
+ => GetString("JsonEntityWithTableSplittingIsNotSupported");
+
+ ///
+ /// JSON property name should only be configured on nested owned navigations.
+ ///
+ public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation
+ => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation");
+
///
/// The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information.
///
@@ -997,6 +1091,12 @@ public static string MappedFunctionNotFound(object? entityType, object? function
public static string MappingFragmentMissingName
=> GetString("MappingFragmentMissingName");
+ ///
+ /// This method needs to be implemented in the provider.
+ ///
+ public static string MethodNeedsToBeImplementedInTheProvider
+ => GetString("MethodNeedsToBeImplementedInTheProvider");
+
///
/// Using '{methodName}' on DbSet of '{entityType}' is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property.
///
@@ -1245,6 +1345,12 @@ public static string PropertyNotMappedToTable(object? property, object? entityTy
GetString("PropertyNotMappedToTable", nameof(property), nameof(entityType), nameof(table)),
property, entityType, table);
+ ///
+ /// 'FromSqlRaw' or 'FromSqlInterpolated' 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 RawSqlQueryNonComposable
+ => GetString("RawSqlQueryNonComposable");
+
///
/// The entity type '{entityType}' is not mapped to a table, therefore the entities cannot be persisted to the database. Call 'ToTable' in 'OnModelCreating' to map it to a table.
///
@@ -1787,112 +1893,6 @@ public static string ViewOverrideMismatch(object? propertySpecification, object?
public static string VisitChildrenMustBeOverridden
=> GetString("VisitChildrenMustBeOverridden");
- ///
- /// Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves.
- ///
- public static string JsonEntityOwnedByNonJsonOwnedType(object? nonJsonType, object? table)
- => string.Format(
- GetString("JsonEntityOwnedByNonJsonOwnedType", nameof(nonJsonType), nameof(table)),
- nonJsonType, table);
-
- ///
- /// Table splitting is not supported for entities containing entities mapped to JSON.
- ///
- public static string JsonEntityWithTableSplittingIsNotSupported
- => GetString("JsonEntityWithTableSplittingIsNotSupported");
-
- ///
- /// Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column.
- ///
- public static string JsonEntityMultipleRootsMappedToTheSameJsonColumn(object? column, object? table)
- => string.Format(
- GetString("JsonEntityMultipleRootsMappedToTheSameJsonColumn", nameof(column), nameof(table)),
- column, table);
-
- ///
- /// Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported.
- ///
- public static string JsonEntityWithOwnerNotMappedToTableOrView(object? entity)
- => string.Format(
- GetString("JsonEntityWithOwnerNotMappedToTableOrView", nameof(entity)),
- entity);
-
- ///
- /// Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner.
- ///
- public static string JsonEntityMappedToDifferentViewThanOwner(object? jsonType, object? viewName, object? ownerType, object? ownerViewName)
- => string.Format(
- GetString("JsonEntityMappedToDifferentViewThanOwner", nameof(jsonType), nameof(viewName), nameof(ownerType), nameof(ownerViewName)),
- jsonType, viewName, ownerType, ownerViewName);
-
- ///
- /// Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities.
- ///
- public static string JsonEntityWithNonTphInheritanceOnOwner(object? rootType, object? tph)
- => string.Format(
- GetString("JsonEntityWithNonTphInheritanceOnOwner", nameof(rootType), nameof(tph)),
- rootType, tph);
-
- ///
- /// Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner.
- ///
- public static string JsonEntityReferencingRegularEntity(object? jsonEntity)
- => string.Format(
- GetString("JsonEntityReferencingRegularEntity", nameof(jsonEntity)),
- jsonEntity);
-
- ///
- /// Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly.
- ///
- public static string JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey(object? keyProperty, object? jsonEntity)
- => string.Format(
- GetString("JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey", nameof(keyProperty), nameof(jsonEntity)),
- keyProperty, jsonEntity);
-
- ///
- /// Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported.
- ///
- public static string JsonEntityWithExplicitlyConfiguredOrdinalKey(object? jsonEntity)
- => string.Format(
- GetString("JsonEntityWithExplicitlyConfiguredOrdinalKey", nameof(jsonEntity)),
- jsonEntity);
-
- ///
- /// Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}.
- ///
- public static string JsonEntityWithIncorrectNumberOfKeyProperties(object? jsonEntity, object? expectedCount, object? actualCount)
- => string.Format(
- GetString("JsonEntityWithIncorrectNumberOfKeyProperties", nameof(jsonEntity), nameof(expectedCount), nameof(actualCount)),
- jsonEntity, expectedCount, actualCount);
-
- ///
- /// Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'.
- ///
- public static string JsonEntityWithDefaultValueSetOnItsProperty(object? jsonEntity, object? property)
- => string.Format(
- GetString("JsonEntityWithDefaultValueSetOnItsProperty", nameof(jsonEntity), nameof(property)),
- jsonEntity, property);
-
- ///
- /// Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property.
- ///
- public static string JsonEntityWithMultiplePropertiesMappedToSameJsonProperty(object? jsonEntity, object? property)
- => string.Format(
- GetString("JsonEntityWithMultiplePropertiesMappedToSameJsonProperty", nameof(jsonEntity), nameof(property)),
- jsonEntity, property);
-
- ///
- /// JSON property name should only be configured on nested owned navigations.
- ///
- public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation
- => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation");
-
- ///
- /// This method needs to be implemented in the provider.
- ///
- public static string MethodNeedsToBeImplementedInTheProvider
- => GetString("MethodNeedsToBeImplementedInTheProvider");
-
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name)!;
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index aab279ce662..369f6daae22 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -403,9 +403,6 @@
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.
-
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}'.
@@ -451,9 +448,6 @@
The specified entity type '{derivedType}' is not derived from '{entityType}'.
-
- A FromSqlExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'.
-
The grouping key '{keySelector}' is of type '{keyType}' which is not valid key.
@@ -478,6 +472,48 @@
The specified 'MinBatchSize' value '{value}' is not valid. It must be a positive number.
+
+ A RawSqlQuerylExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'.
+
+
+ Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner.
+
+
+ Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column.
+
+
+ Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves.
+
+
+ Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner.
+
+
+ Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'.
+
+
+ Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly.
+
+
+ Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported.
+
+
+ Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}.
+
+
+ Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property.
+
+
+ Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities.
+
+
+ Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported.
+
+
+ Table splitting is not supported for entities containing entities mapped to JSON.
+
+
+ JSON property name should only be configured on nested owned navigations.
+
The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information.
@@ -774,6 +810,9 @@
Table name must be specified to configure a table-specific property mapping.
+
+ This method needs to be implemented in the provider.
+
Using '{methodName}' on DbSet of '{entityType}' is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property.
@@ -876,6 +915,9 @@
The property '{property}' on entity type '{entityType}' is not mapped to '{table}'.
+
+ 'FromSqlRaw' or 'FromSqlInterpolated' 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 entity type '{entityType}' is not mapped to a table, therefore the entities cannot be persisted to the database. Call 'ToTable' in 'OnModelCreating' to map it to a table.
@@ -1086,46 +1128,4 @@
'VisitChildren' must be overridden in the class deriving from 'SqlExpression'.
-
- Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves.
-
-
- Table splitting is not supported for entities containing entities mapped to JSON.
-
-
- Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column.
-
-
- Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported.
-
-
- Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner.
-
-
- Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities.
-
-
- Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner.
-
-
- Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly.
-
-
- Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported.
-
-
- Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}.
-
-
- Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'.
-
-
- Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property.
-
-
- JSON property name should only be configured on nested owned navigations.
-
-
- This method needs to be implemented in the provider.
-
\ No newline at end of file
diff --git a/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs b/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
index 3956a68acef..cb391b94d84 100644
--- a/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
+++ b/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
@@ -1249,7 +1249,7 @@ private void InitializeFields()
if (_columns.Count > 0
&& _columns.Any(e => e?.Name != null))
{
- // Non-Composed FromSql
+ // Non-Composed RawSqlQuery
var readerColumns = _fieldNameLookup.Value;
_indexMap = new int[_columns.Count];
@@ -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/FromSqlParameterExpandingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RawSqlParameterExpandingExpressionVisitor.cs
similarity index 88%
rename from src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs
rename to src/EFCore.Relational/Query/Internal/RawSqlParameterExpandingExpressionVisitor.cs
index 78a51e11b72..d52b1298ca8 100644
--- a/src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/Internal/RawSqlParameterExpandingExpressionVisitor.cs
@@ -14,10 +14,10 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal;
/// 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 class FromSqlParameterExpandingExpressionVisitor : ExpressionVisitor
+public class RawSqlParameterExpandingExpressionVisitor : ExpressionVisitor
{
- private readonly IDictionary _visitedFromSqlExpressions
- = new Dictionary(LegacyReferenceEqualityComparer.Instance);
+ private readonly IDictionary _visitedRawSqlExpressions
+ = new Dictionary(LegacyReferenceEqualityComparer.Instance);
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;
@@ -33,7 +33,7 @@ private readonly IDictionary _visitedFromSqlExpre
/// 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 FromSqlParameterExpandingExpressionVisitor(
+ public RawSqlParameterExpandingExpressionVisitor(
RelationalParameterBasedSqlProcessorDependencies dependencies)
{
Dependencies = dependencies;
@@ -61,7 +61,7 @@ public virtual Expression Expand(
IReadOnlyDictionary parameterValues,
out bool canCache)
{
- _visitedFromSqlExpressions.Clear();
+ _visitedRawSqlExpressions.Clear();
_parameterNameGenerator = _parameterNameGeneratorFactory.Create();
_parametersValues = parameterValues;
_canCache = true;
@@ -81,17 +81,17 @@ public virtual Expression Expand(
[return: NotNullIfNotNull("expression")]
public override Expression? Visit(Expression? expression)
{
- if (expression is not FromSqlExpression fromSql)
+ if (expression is not RawSqlQueryExpression rawSqlQuery)
{
return base.Visit(expression);
}
- if (_visitedFromSqlExpressions.TryGetValue(fromSql, out var visitedFromSql))
+ if (_visitedRawSqlExpressions.TryGetValue(rawSqlQuery, out var visitedRawSql))
{
- return visitedFromSql;
+ return visitedRawSql;
}
- switch (fromSql.Arguments)
+ switch (rawSqlQuery.Arguments)
{
case ParameterExpression parameterExpression:
// parameter value will never be null. It could be empty object?[]
@@ -127,7 +127,7 @@ public virtual Expression Expand(
}
}
- return _visitedFromSqlExpressions[fromSql] = fromSql.Update(
+ return _visitedRawSqlExpressions[rawSqlQuery] = rawSqlQuery.Update(
Expression.Constant(new CompositeRelationalParameter(parameterExpression.Name!, subParameters)));
case ConstantExpression constantExpression:
@@ -157,10 +157,10 @@ public virtual Expression Expand(
}
}
- return _visitedFromSqlExpressions[fromSql] = fromSql.Update(Expression.Constant(constantValues, typeof(object[])));
+ return _visitedRawSqlExpressions[rawSqlQuery] = rawSqlQuery.Update(Expression.Constant(constantValues, typeof(object[])));
default:
- Check.DebugFail("FromSql.Arguments must be Constant/ParameterExpression");
+ Check.DebugFail("RawSqlQueryExpression.Arguments must be Constant/ParameterExpression");
return null;
}
}
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/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index 647bb0afcf1..af7f158624d 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -85,9 +85,9 @@ protected virtual void GenerateRootCommand(Expression queryExpression)
case SelectExpression selectExpression:
GenerateTagsHeaderComment(selectExpression);
- if (selectExpression.IsNonComposedFromSql())
+ if (selectExpression.IsNonComposedRawSqlQuery())
{
- GenerateFromSql((FromSqlExpression)selectExpression.Tables[0]);
+ GenerateRawSqlQuery((RawSqlQueryExpression)selectExpression.Tables[0]);
}
else
{
@@ -383,12 +383,12 @@ protected override Expression VisitTable(TableExpression tableExpression)
return tableExpression;
}
- private void GenerateFromSql(FromSqlExpression fromSqlExpression)
+ private void GenerateRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression)
{
- var sql = fromSqlExpression.Sql;
+ var sql = rawSqlQueryExpression.Sql;
string[]? substitutions;
- switch (fromSqlExpression.Arguments)
+ switch (rawSqlQueryExpression.Arguments)
{
case ConstantExpression { Value: CompositeRelationalParameter compositeRelationalParameter }:
{
@@ -426,11 +426,11 @@ private void GenerateFromSql(FromSqlExpression fromSqlExpression)
default:
throw new ArgumentOutOfRangeException(
- nameof(fromSqlExpression),
- fromSqlExpression.Arguments,
- RelationalStrings.InvalidFromSqlArguments(
- fromSqlExpression.Arguments.GetType(),
- fromSqlExpression.Arguments is ConstantExpression constantExpression
+ nameof(rawSqlQueryExpression),
+ rawSqlQueryExpression.Arguments,
+ RelationalStrings.InvalidRawSqlQueryArguments(
+ rawSqlQueryExpression.Arguments.GetType(),
+ rawSqlQueryExpression.Arguments is ConstantExpression constantExpression
? constantExpression.Value?.GetType()
: null));
}
@@ -443,6 +443,7 @@ fromSqlExpression.Arguments is ConstantExpression constantExpression
}
///
+ [Obsolete]
protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression)
{
_relationalCommandBuilder.AppendLine("(");
@@ -451,7 +452,7 @@ protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression)
using (_relationalCommandBuilder.Indent())
{
- GenerateFromSql(fromSqlExpression);
+ GenerateRawSqlQuery(fromSqlExpression);
}
_relationalCommandBuilder.Append(")")
@@ -461,6 +462,25 @@ protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression)
return fromSqlExpression;
}
+ ///
+ protected override Expression VisitRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression)
+ {
+ _relationalCommandBuilder.AppendLine("(");
+
+ CheckComposableSql(rawSqlQueryExpression.Sql);
+
+ using (_relationalCommandBuilder.Indent())
+ {
+ GenerateRawSqlQuery(rawSqlQueryExpression);
+ }
+
+ _relationalCommandBuilder.Append(")")
+ .Append(AliasSeparator)
+ .Append(_sqlGenerationHelper.DelimitIdentifier(rawSqlQueryExpression.Alias));
+
+ return rawSqlQueryExpression;
+ }
+
///
/// Checks whether a given SQL string is composable, i.e. can be embedded as a subquery within a
/// larger SQL query.
@@ -479,7 +499,7 @@ protected virtual void CheckComposableSql(string sql)
var i = span.IndexOf('\n');
span = i > 0
? span[(i + 1)..].TrimStart()
- : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);
+ : throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable);
continue;
}
@@ -489,7 +509,7 @@ protected virtual void CheckComposableSql(string sql)
var i = span.IndexOf("*/");
span = i > 0
? span[(i + 2)..].TrimStart()
- : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);
+ : throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable);
continue;
}
@@ -511,7 +531,7 @@ protected virtual void CheckComposableSqlTrimmed(ReadOnlySpan sql)
? sql["SELECT".Length..]
: sql.StartsWith("WITH", StringComparison.OrdinalIgnoreCase)
? sql["WITH".Length..]
- : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);
+ : throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable);
if (sql.Length > 0
&& (char.IsWhiteSpace(sql[0]) || sql.StartsWith("--") || sql.StartsWith("/*")))
@@ -519,7 +539,7 @@ protected virtual void CheckComposableSqlTrimmed(ReadOnlySpan sql)
return;
}
- throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);
+ throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable);
}
///
diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs
index 14f598e221f..0d2ff815476 100644
--- a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs
+++ b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs
@@ -56,8 +56,8 @@ public virtual Expression Optimize(
queryExpression = ProcessSqlNullability(queryExpression, parametersValues, out var sqlNullablityCanCache);
canCache &= sqlNullablityCanCache;
- queryExpression = ExpandFromSqlParameter(queryExpression, parametersValues, out var fromSqlParameterCanCache);
- canCache &= fromSqlParameterCanCache;
+ queryExpression = ExpandRawSqlQueryParameter(queryExpression, parametersValues, out var rawSqlQueryParameterCanCache);
+ canCache &= rawSqlQueryParameterCanCache;
return queryExpression;
}
@@ -83,9 +83,23 @@ protected virtual Expression ProcessSqlNullability(
/// A dictionary of parameter values to use.
/// A bool value indicating if the query expression can be cached.
/// A processed query expression.
+ [Obsolete("Use ExpandRawSqlQueryParameter")]
protected virtual Expression ExpandFromSqlParameter(
Expression queryExpression,
IReadOnlyDictionary parametersValues,
out bool canCache)
- => new FromSqlParameterExpandingExpressionVisitor(Dependencies).Expand(queryExpression, parametersValues, out canCache);
+ => new RawSqlParameterExpandingExpressionVisitor(Dependencies).Expand(queryExpression, parametersValues, out canCache);
+
+ ///
+ /// Expands the parameters to inside the query expression for given parameter values.
+ ///
+ /// A query expression to optimize.
+ /// A dictionary of parameter values to use.
+ /// A bool value indicating if the query expression can be cached.
+ /// A processed query expression.
+ protected virtual Expression ExpandRawSqlQueryParameter(
+ Expression queryExpression,
+ IReadOnlyDictionary parametersValues,
+ out bool canCache)
+ => new RawSqlParameterExpandingExpressionVisitor(Dependencies).Expand(queryExpression, parametersValues, out canCache);
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 82f2f74a278..76d8359828c 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,29 @@ 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();
+ }
+
+ var selectExpression = new SelectExpression(sqlQueryRootExpression.Type, typeMapping,
+ new RawSqlQueryExpression("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/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
index 7c3a5ffe687..7ae2fa95af5 100644
--- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
@@ -229,7 +229,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery
var selectExpression = (SelectExpression)shapedQueryExpression.QueryExpression;
VerifyNoClientConstant(shapedQueryExpression.ShaperExpression);
- var nonComposedFromSql = selectExpression.IsNonComposedFromSql();
+ var nonComposedFromSql = selectExpression.IsNonComposedRawSqlQuery();
var querySplittingBehavior = ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior;
var splitQuery = querySplittingBehavior == QuerySplittingBehavior.SplitQuery;
var collectionCount = 0;
diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
index 2aa4c1fe8f8..0cd81a4d8f1 100644
--- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
@@ -55,7 +55,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
return VisitExists(existsExpression);
case FromSqlExpression fromSqlExpression:
+#pragma warning disable CS0618 // Type or member is obsolete
return VisitFromSql(fromSqlExpression);
+#pragma warning restore CS0618 // Type or member is obsolete
case InExpression inExpression:
return VisitIn(inExpression);
@@ -81,8 +83,8 @@ protected override Expression VisitExtension(Expression extensionExpression)
case ProjectionExpression projectionExpression:
return VisitProjection(projectionExpression);
- case TableValuedFunctionExpression tableValuedFunctionExpression:
- return VisitTableValuedFunction(tableValuedFunctionExpression);
+ case RawSqlQueryExpression rawSqlQueryExpression:
+ return VisitRawSqlQuery(rawSqlQueryExpression);
case RowNumberExpression rowNumberExpression:
return VisitRowNumber(rowNumberExpression);
@@ -114,6 +116,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
case TableExpression tableExpression:
return VisitTable(tableExpression);
+ case TableValuedFunctionExpression tableValuedFunctionExpression:
+ return VisitTableValuedFunction(tableValuedFunctionExpression);
+
case UnionExpression unionExpression:
return VisitUnion(unionExpression);
}
@@ -197,8 +202,16 @@ protected override Expression VisitExtension(Expression extensionExpression)
///
/// The expression to visit.
/// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
+ [Obsolete("Use VisitRawSqlQuery")]
protected abstract Expression VisitFromSql(FromSqlExpression fromSqlExpression);
+ ///
+ /// Visits the children of the raw sql query expression.
+ ///
+ /// The expression to visit.
+ /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
+ protected abstract Expression VisitRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression);
+
///
/// Visits the children of the in expression.
///
diff --git a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
index 344ff9bb7f8..683384d3abc 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
@@ -1,20 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Diagnostics.CodeAnalysis;
-
namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
///
///
-/// An expression that represents a subquery table source with user-provided custom SQL.
+/// An expression that represents a subquery table source projecting entity with user-provided custom SQL.
///
///
/// This type is typically used by database providers (and other extensions). It is generally
/// not used in application code.
///
///
-public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBase, ITableBasedExpression
+public class FromSqlExpression : RawSqlQueryExpression, ITableBasedExpression
{
private readonly ITableBase _table;
@@ -25,8 +23,9 @@ public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBa
/// A user-provided custom SQL for the table source.
/// A user-provided parameters to pass to the custom SQL.
public FromSqlExpression(ITableBase defaultTableBase, string sql, Expression arguments)
- : this(defaultTableBase.Name[..1].ToLowerInvariant(), defaultTableBase, sql, arguments, annotations: null)
+ : base(defaultTableBase.Name[..1].ToLowerInvariant(), sql, arguments)
{
+ _table = defaultTableBase;
}
// See issue#21660/21627
@@ -45,40 +44,18 @@ private FromSqlExpression(
string sql,
Expression arguments,
IEnumerable? annotations)
- : base(alias, annotations)
+ : base(alias, sql, arguments, annotations)
{
_table = tableBase;
- Sql = sql;
- Arguments = arguments;
- }
-
- ///
- /// The alias assigned to this table source.
- ///
- [NotNull]
- public override string? Alias
- {
- get => base.Alias!;
- internal set => base.Alias = value;
}
- ///
- /// The user-provided custom SQL for the table source.
- ///
- public virtual string Sql { get; }
-
- ///
- /// The user-provided parameters passed to the custom SQL.
- ///
- public virtual Expression Arguments { 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
/// return this expression.
///
- /// The property of the result.
+ /// The property of the result.
/// This expression if no children changed, or an expression with the updated children.
- public virtual FromSqlExpression Update(Expression arguments)
+ public override FromSqlExpression Update(Expression arguments)
=> arguments != Arguments
? new FromSqlExpression(Alias, _table, Sql, arguments, GetAnnotations())
: this;
@@ -87,34 +64,6 @@ public virtual FromSqlExpression Update(Expression arguments)
ITableBase ITableBasedExpression.Table => _table;
///
- protected override Expression VisitChildren(ExpressionVisitor visitor)
- => this;
-
- ///
- public virtual TableExpressionBase Clone()
+ public override TableExpressionBase Clone()
=> new FromSqlExpression(Alias, _table, Sql, Arguments, GetAnnotations());
-
- ///
- protected override void Print(ExpressionPrinter expressionPrinter)
- {
- expressionPrinter.Append(Sql);
- PrintAnnotations(expressionPrinter);
- }
-
- ///
- public override bool Equals(object? obj)
- => obj != null
- && (ReferenceEquals(this, obj)
- || obj is FromSqlExpression fromSqlExpression
- && Equals(fromSqlExpression));
-
- private bool Equals(FromSqlExpression fromSqlExpression)
- => base.Equals(fromSqlExpression)
- && _table == fromSqlExpression._table
- && Sql == fromSqlExpression.Sql
- && ExpressionEqualityComparer.Instance.Equals(Arguments, fromSqlExpression.Arguments);
-
- ///
- public override int GetHashCode()
- => HashCode.Combine(base.GetHashCode(), Sql);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/RawSqlQueryExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/RawSqlQueryExpression.cs
new file mode 100644
index 00000000000..5a08f03363e
--- /dev/null
+++ b/src/EFCore.Relational/Query/SqlExpressions/RawSqlQueryExpression.cs
@@ -0,0 +1,109 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+///
+///
+/// An expression that represents a subquery table source with user-provided custom SQL.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class RawSqlQueryExpression : TableExpressionBase, IClonableTableExpressionBase
+{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// A user-provided custom SQL for the table source.
+ /// A user-provided parameters to pass to the custom SQL.
+ public RawSqlQueryExpression(string alias, string sql, Expression arguments)
+ : this(alias, sql, arguments, annotations: null)
+ {
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// A user-provided custom SQL for the table source.
+ /// A user-provided parameters to pass to the custom SQL.
+ /// A collection of annotations associated with this expression.
+ protected RawSqlQueryExpression(
+ string alias,
+ string sql,
+ Expression arguments,
+ IEnumerable? annotations)
+ : base(alias, annotations)
+ {
+ Sql = sql;
+ Arguments = arguments;
+ }
+
+ ///
+ /// The alias assigned to this table source.
+ ///
+ [NotNull]
+ public override string? Alias
+ {
+ get => base.Alias!;
+ internal set => base.Alias = value;
+ }
+
+ ///
+ /// The user-provided custom SQL for the table source.
+ ///
+ public virtual string Sql { get; }
+
+ ///
+ /// The user-provided parameters passed to the custom SQL.
+ ///
+ public virtual Expression Arguments { 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
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// This expression if no children changed, or an expression with the updated children.
+ public virtual RawSqlQueryExpression Update(Expression arguments)
+ => arguments != Arguments
+ ? new RawSqlQueryExpression(Alias, Sql, arguments, GetAnnotations())
+ : this;
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ => this;
+
+ ///
+ public virtual TableExpressionBase Clone()
+ => new RawSqlQueryExpression(Alias, Sql, Arguments, GetAnnotations());
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append(Sql);
+ PrintAnnotations(expressionPrinter);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj != null
+ && (ReferenceEquals(this, obj)
+ || obj is RawSqlQueryExpression sqlQueryExpression
+ && Equals(sqlQueryExpression));
+
+ private bool Equals(RawSqlQueryExpression sqlQueryExpression)
+ => base.Equals(sqlQueryExpression)
+ && Sql == sqlQueryExpression.Sql
+ && ExpressionEqualityComparer.Instance.Equals(Arguments, sqlQueryExpression.Arguments);
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(base.GetHashCode(), Sql);
+}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index 8ed2e6ad81a..4e4cd1822d3 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 =
@@ -91,6 +92,18 @@ internal SelectExpression(SqlExpression? projection)
}
}
+ internal SelectExpression(Type type, RelationalTypeMapping typeMapping, RawSqlQueryExpression sqlQueryExpression)
+ : base(null)
+ {
+ var tableReferenceExpression = new TableReferenceExpression(this, sqlQueryExpression.Alias!);
+ AddTable(sqlQueryExpression, tableReferenceExpression);
+
+ var columnExpression = new ConcreteColumnExpression(
+ SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType());
+
+ _projectionMapping[new ProjectionMember()] = columnExpression;
+ }
+
internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpressionFactory)
: base(null)
{
@@ -3186,6 +3199,7 @@ EntityProjectionExpression LiftEntityProjectionFromSubquery(EntityProjectionExpr
/// Checks whether this represents a which is not composed upon.
///
/// A bool value indicating a non-composed .
+ [Obsolete("Use IsNonComposedRawSqlQuery")]
public bool IsNonComposedFromSql()
=> Limit == null
&& Offset == null
@@ -3202,6 +3216,26 @@ public bool IsNonComposedFromSql()
&& _projectionMapping.TryGetValue(new ProjectionMember(), out var mapping)
&& mapping.Type == typeof(Dictionary);
+ ///
+ /// Checks whether this represents a which is not composed upon.
+ ///
+ /// A bool value indicating a non-composed .
+ public bool IsNonComposedRawSqlQuery()
+ => Limit == null
+ && Offset == null
+ && !IsDistinct
+ && Predicate == null
+ && GroupBy.Count == 0
+ && Having == null
+ && Orderings.Count == 0
+ && Tables.Count == 1
+ && Tables[0] is RawSqlQueryExpression rawSqlQuery
+ && Projection.All(
+ pe => pe.Expression is ColumnExpression column
+ && string.Equals(rawSqlQuery.Alias, column.TableAlias, StringComparison.OrdinalIgnoreCase))
+ && _projectionMapping.TryGetValue(new ProjectionMember(), out var mapping)
+ && (mapping.Type == typeof(Dictionary) || mapping.Type == typeof(int));
+
///
/// Prepares the to apply aggregate operation over it.
///
diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
index 7531af3a256..6d9569b0dd7 100644
--- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
+++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
@@ -123,8 +123,8 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB
return exceptExpression.Update(source1, source2);
}
- case FromSqlExpression fromSqlExpression:
- return fromSqlExpression;
+ case RawSqlQueryExpression rawSqlQueryExpression:
+ return rawSqlQueryExpression;
case InnerJoinExpression innerJoinExpression:
{
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.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
index f45b7632d43..d9ca62ce581 100644
--- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
@@ -207,9 +207,19 @@ protected override Expression VisitExists(ExistsExpression existsExpression)
/// 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.
///
+ [Obsolete]
protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression)
=> fromSqlExpression;
+ ///
+ /// 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 VisitRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression)
+ => rawSqlQueryExpression;
+
///
/// 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
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index 945262affac..391aec012dd 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -246,7 +246,7 @@ protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql)
if (sql.StartsWith("WITH", StringComparison.OrdinalIgnoreCase))
{
- throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);
+ throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable);
}
}
diff --git a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs
index c9fe80f5866..224192a2a19 100644
--- a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs
+++ b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs
@@ -64,5 +64,11 @@ 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/FromSqlSprocQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs
index 02caac1cb2b..a962bc9048a 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs
@@ -142,7 +142,7 @@ public virtual async Task From_sql_queryable_stored_procedure_projection(bool as
.Select(mep => mep.TenMostExpensiveProducts);
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -162,7 +162,7 @@ public virtual async Task From_sql_queryable_stored_procedure_re_projection(bool
new MostExpensiveProduct { TenMostExpensiveProducts = "Foo", UnitPrice = mep.UnitPrice });
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -222,7 +222,7 @@ public virtual async Task From_sql_queryable_stored_procedure_composed(bool asyn
.OrderBy(mep => mep.UnitPrice);
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -264,7 +264,7 @@ public virtual async Task From_sql_queryable_stored_procedure_with_parameter_com
.OrderBy(coh => coh.Total);
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -305,7 +305,7 @@ public virtual async Task From_sql_queryable_stored_procedure_take(bool async)
.Take(2);
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -343,7 +343,7 @@ public virtual async Task From_sql_queryable_stored_procedure_min(bool async)
.FromSqlRaw(TenMostExpensiveProductsSproc, GetTenMostExpensiveProductsParameters());
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.MinAsync(mep => mep.UnitPrice))
: Assert.Throws(() => query.Min(mep => mep.UnitPrice))).Message);
@@ -377,7 +377,7 @@ public virtual async Task From_sql_queryable_stored_procedure_with_include_throw
.Include(p => p.OrderDetails);
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -397,7 +397,7 @@ from b in context.Set()
select new { a, b };
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -440,7 +440,7 @@ from p in context.Set()
select new { mep, p };
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
@@ -481,7 +481,7 @@ from mep in context.Set()
select new { mep, p };
Assert.Equal(
- RelationalStrings.FromSqlNonComposable,
+ RelationalStrings.RawSqlQueryNonComposable,
(async
? await Assert.ThrowsAsync(() => query.ToArrayAsync())
: Assert.Throws(() => query.ToArray())).Message);
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..eabe64fdbda
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSqlQueryTestBase.cs
@@ -0,0 +1,48 @@
+// 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