diff --git a/All.sln.DotSettings b/All.sln.DotSettings
index 8229c429771..762d1ed2be6 100644
--- a/All.sln.DotSettings
+++ b/All.sln.DotSettings
@@ -308,8 +308,10 @@ The .NET Foundation licenses this file to you under the MIT license.
TrueTrueTrue
+ TrueTrueTrue
+ TrueTrueTrueTrue
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 5de1ff3e3fa..7f71b06b607 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -621,6 +621,12 @@ public static string EitherOfTwoValuesMustBeNull(object? param1, object? param2)
GetString("EitherOfTwoValuesMustBeNull", nameof(param1), nameof(param2)),
param1, param2);
+ ///
+ /// Empty constant collections are not supported as constant query roots.
+ ///
+ public static string EmptyCollectionNotSupportedAsConstantQueryRoot
+ => GetString("EmptyCollectionNotSupportedAsConstantQueryRoot");
+
///
/// The short name for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type in the hierarchy must have a unique short name. Either rename one of the types or call modelBuilder.Entity<TEntity>().Metadata.SetDiscriminatorValue("NewShortName").
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index e89cace8c09..23fd4947e4a 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -346,6 +346,9 @@
Either {param1} or {param2} must be null.
+
+ Empty constant collections are not supported as constant query roots.
+
The short name for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type in the hierarchy must have a unique short name. Either rename one of the types or call modelBuilder.Entity<TEntity>().Metadata.SetDiscriminatorValue("NewShortName").
diff --git a/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs b/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
index 0e523bf70a9..804a84ab14c 100644
--- a/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
+++ b/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
@@ -57,11 +57,10 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
}
private static bool ValidateValues(SqlExpression values)
- => values is SqlConstantExpression || values is SqlParameterExpression;
+ => values is SqlConstantExpression or SqlParameterExpression;
private static SqlExpression RemoveObjectConvert(SqlExpression expression)
- => expression is SqlUnaryExpression sqlUnaryExpression
- && sqlUnaryExpression.OperatorType == ExpressionType.Convert
+ => expression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } sqlUnaryExpression
&& sqlUnaryExpression.Type == typeof(object)
? sqlUnaryExpression.Operand
: expression;
diff --git a/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs
index fb9496b2867..a8973e3a86a 100644
--- a/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs
@@ -35,11 +35,11 @@ public SelectExpressionProjectionApplyingExpressionVisitor(QuerySplittingBehavio
protected override Expression VisitExtension(Expression extensionExpression)
=> extensionExpression switch
{
- ShapedQueryExpression shapedQueryExpression
- when shapedQueryExpression.QueryExpression is SelectExpression selectExpression
+ ShapedQueryExpression { QueryExpression: SelectExpression selectExpression } shapedQueryExpression
=> shapedQueryExpression.UpdateShaperExpression(
selectExpression.ApplyProjection(
shapedQueryExpression.ShaperExpression, shapedQueryExpression.ResultCardinality, _querySplittingBehavior)),
+
NonQueryExpression nonQueryExpression => nonQueryExpression,
_ => base.VisitExtension(extensionExpression),
};
diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index 07855455d00..c7075b3a250 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -1,7 +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 System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage.Internal;
@@ -169,21 +168,22 @@ protected override Expression VisitSqlFragment(SqlFragmentExpression sqlFragment
}
private static bool IsNonComposedSetOperation(SelectExpression selectExpression)
- => selectExpression.Offset == null
- && selectExpression.Limit == null
- && !selectExpression.IsDistinct
- && selectExpression.Predicate == null
- && selectExpression.Having == null
- && selectExpression.Orderings.Count == 0
- && selectExpression.GroupBy.Count == 0
- && selectExpression.Tables.Count == 1
- && selectExpression.Tables[0] is SetOperationBase setOperation
+ => selectExpression is
+ {
+ Tables: [SetOperationBase setOperation],
+ Offset: null,
+ Limit: null,
+ IsDistinct: false,
+ Predicate: null,
+ Having: null,
+ Orderings.Count: 0,
+ GroupBy.Count: 0
+ }
&& selectExpression.Projection.Count == setOperation.Source1.Projection.Count
&& selectExpression.Projection.Select(
(pe, index) => pe.Expression is ColumnExpression column
- && string.Equals(column.TableAlias, setOperation.Alias, StringComparison.Ordinal)
- && string.Equals(
- column.Name, setOperation.Source1.Projection[index].Alias, StringComparison.Ordinal))
+ && column.TableAlias == setOperation.Alias
+ && column.Name == setOperation.Source1.Projection[index].Alias)
.All(e => e);
///
@@ -226,12 +226,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
subQueryIndent = _relationalCommandBuilder.Indent();
}
- if (IsNonComposedSetOperation(selectExpression))
- {
- // Naked set operation
- GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
- }
- else
+ if (!TryGenerateWithoutWrappingSelect(selectExpression))
{
_relationalCommandBuilder.Append("SELECT ");
@@ -300,6 +295,43 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
return selectExpression;
}
+ ///
+ /// If possible, generates the expression contained within the provided without the wrapping
+ /// SELECT. This can be done for set operations and VALUES, which can appear as top-level statements without needing to be wrapped
+ /// in SELECT.
+ ///
+ protected virtual bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
+ {
+ if (IsNonComposedSetOperation(selectExpression))
+ {
+ GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
+ return true;
+ }
+
+ if (selectExpression is
+ {
+ Tables: [ValuesExpression valuesExpression],
+ Offset: null,
+ Limit: null,
+ IsDistinct: false,
+ Predicate: null,
+ Having: null,
+ Orderings.Count: 0,
+ GroupBy.Count: 0,
+ }
+ && selectExpression.Projection.Count == valuesExpression.ColumnNames.Count
+ && selectExpression.Projection.Select(
+ (pe, index) => pe.Expression is ColumnExpression column
+ && column.Name == valuesExpression.ColumnNames[index])
+ .All(e => e))
+ {
+ GenerateValues(valuesExpression);
+ return true;
+ }
+
+ return false;
+ }
+
///
/// Generates a pseudo FROM clause. Required by some providers when a query has no actual FROM clause.
///
@@ -312,9 +344,7 @@ protected virtual void GeneratePseudoFromClause()
///
/// SelectExpression for which the empty projection will be generated.
protected virtual void GenerateEmptyProjection(SelectExpression selectExpression)
- {
- _relationalCommandBuilder.Append("1");
- }
+ => _relationalCommandBuilder.Append("1");
///
protected override Expression VisitProjection(ProjectionExpression projectionExpression)
@@ -371,16 +401,16 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
///
protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression)
{
- if (!string.IsNullOrEmpty(tableValuedFunctionExpression.StoreFunction.Schema))
+ if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema))
{
_relationalCommandBuilder
- .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Schema))
+ .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema))
.Append(".");
}
- var name = tableValuedFunctionExpression.StoreFunction.IsBuiltIn
- ? tableValuedFunctionExpression.StoreFunction.Name
- : _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Name);
+ var name = tableValuedFunctionExpression.IsBuiltIn
+ ? tableValuedFunctionExpression.Name
+ : _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name);
_relationalCommandBuilder
.Append(name)
@@ -1132,6 +1162,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
return rowNumberExpression;
}
+ ///
+ protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
+ {
+ Sql.Append("(");
+
+ var values = rowValueExpression.Values;
+ var count = values.Count;
+ for (var i = 0; i < count; i++)
+ {
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Visit(values[i]);
+ }
+
+ Sql.Append(")");
+
+ return rowValueExpression;
+ }
+
///
/// Generates a set operation in the relational command.
///
@@ -1141,18 +1193,16 @@ protected virtual void GenerateSetOperation(SetOperationBase setOperation)
GenerateSetOperationOperand(setOperation, setOperation.Source1);
_relationalCommandBuilder
.AppendLine()
- .Append(GetSetOperation(setOperation))
+ .Append(
+ setOperation switch
+ {
+ ExceptExpression => "EXCEPT",
+ IntersectExpression => "INTERSECT",
+ UnionExpression => "UNION",
+ _ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
+ })
.AppendLine(setOperation.IsDistinct ? string.Empty : " ALL");
GenerateSetOperationOperand(setOperation, setOperation.Source2);
-
- static string GetSetOperation(SetOperationBase operation)
- => operation switch
- {
- ExceptExpression => "EXCEPT",
- IntersectExpression => "INTERSECT",
- UnionExpression => "UNION",
- _ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType"))
- };
}
///
@@ -1311,6 +1361,66 @@ void LiftPredicate(TableExpressionBase joinTable)
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
}
+ ///
+ protected override Expression VisitValues(ValuesExpression valuesExpression)
+ {
+ _relationalCommandBuilder.Append("(");
+
+ GenerateValues(valuesExpression);
+
+ _relationalCommandBuilder
+ .Append(")")
+ .Append(AliasSeparator)
+ .Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias));
+
+ return valuesExpression;
+ }
+
+ ///
+ /// Generates a VALUES expression.
+ ///
+ protected virtual void GenerateValues(ValuesExpression valuesExpression)
+ {
+ var rowValues = valuesExpression.RowValues;
+
+ // Some databases support providing the names of columns projected out of VALUES, e.g.
+ // SQL Server/PG: (VALUES (1, 3), (2, 4)) AS x(a, b). Others unfortunately don't; so by default, we extract out the first row,
+ // and generate a SELECT for it with the names, and a UNION ALL over the rest of the values.
+ _relationalCommandBuilder.Append("SELECT ");
+
+ Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0");
+ var firstRowValues = rowValues[0].Values;
+ for (var i = 0; i < firstRowValues.Count; i++)
+ {
+ if (i > 0)
+ {
+ _relationalCommandBuilder.Append(", ");
+ }
+
+ Visit(firstRowValues[i]);
+
+ _relationalCommandBuilder
+ .Append(AliasSeparator)
+ .Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
+ }
+
+ if (rowValues.Count > 1)
+ {
+ _relationalCommandBuilder.Append(" UNION ALL VALUES ");
+
+ for (var i = 1; i < rowValues.Count; i++)
+ {
+ // TODO: Do we want newlines here?
+ if (i > 1)
+ {
+ _relationalCommandBuilder.Append(", ");
+ }
+
+ Visit(valuesExpression.RowValues[i]);
+ }
+ }
+ }
+
///
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
=> throw new InvalidOperationException(
diff --git a/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs
new file mode 100644
index 00000000000..1c2ddf74205
--- /dev/null
+++ b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs
@@ -0,0 +1,84 @@
+// 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.Query.Internal;
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+///
+public class RelationalQueryRootProcessor : QueryRootProcessor
+{
+ private readonly ITypeMappingSource _typeMappingSource;
+ private readonly IModel _model;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The type mapping source.
+ /// The model.
+ public RelationalQueryRootProcessor(ITypeMappingSource typeMappingSource, IModel model)
+ {
+ _typeMappingSource = typeMappingSource;
+ _model = model;
+ }
+
+ ///
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ // Create query root node for table-valued functions
+ if (_model.FindDbFunction(methodCallExpression.Method) is { IsScalar: false, StoreFunction: var storeFunction })
+ {
+ // See issue #19970
+ return new TableValuedFunctionQueryRootExpression(
+ storeFunction.EntityTypeMappings.Single().EntityType,
+ storeFunction,
+ methodCallExpression.Arguments);
+ }
+
+ return base.VisitMethodCall(methodCallExpression);
+ }
+
+ ///
+ /// Given a queryable constants over an element type which has a type mapping, converts it to a
+ /// ; it will be translated to a SQL .
+ ///
+ /// The constant expression to attempt to convert to a query root.
+ protected override Expression VisitQueryableConstant(ConstantExpression constantExpression)
+ // TODO: Note that we restrict to constants whose element type is mappable as-is. This excludes a constant list with an unsupported
+ // CLR type, where we could infer a type mapping with a value converter based on its later usage. However, converting all constant
+ // collections to query roots also carries risk.
+ => constantExpression.Type.TryGetSequenceType() is Type elementType && _typeMappingSource.FindMapping(elementType) is not null
+ ? new ConstantQueryRootExpression(constantExpression)
+ : constantExpression;
+
+ ///
+ protected override Expression VisitQueryableParameter(ParameterExpression parameterExpression)
+ {
+ // TODO: Decide whether this belongs here or in specific provider code. This means parameter query roots always get created
+ // (in enumerable/queryable context), but may not be translatable in the provider's QueryableMethodTranslatingEV. As long
+ // as we unwrap there, we *should* be OK, and so don't need an additional provider extension point here...
+
+ // TODO: Also, maybe this type checking should be in the base class.
+ // SQL Server's OpenJson, which we use to unpack the queryable parameter, does not support geometry (or any other non-built-in
+ // types)
+
+ // We convert to query roots only parameters whose CLR type has a collection type mapping (i.e. ElementTypeMapping isn't null).
+ // This allows the provider to determine exactly which types are supported as queryable collections (e.g. OPENJSON on SQL Server).
+ return _typeMappingSource.FindMapping(parameterExpression.Type) is { ElementTypeMapping: not null }
+ ? new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression)
+ : parameterExpression;
+ }
+
+ ///
+ protected override Expression VisitExtension(Expression node)
+ => node switch
+ {
+ // We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert
+ // that to a query root
+ FromSqlQueryRootExpression e => e,
+
+ _ => base.VisitExtension(node)
+ };
+
+}
diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
index 6077a62c474..03aef7b245d 100644
--- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
@@ -36,9 +36,13 @@ public override Expression NormalizeQueryableMethod(Expression expression)
{
expression = new RelationalQueryMetadataExtractingExpressionVisitor(_relationalQueryCompilationContext).Visit(expression);
expression = base.NormalizeQueryableMethod(expression);
- expression = new TableValuedFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression);
expression = new CollectionIndexerToElementAtNormalizingExpressionVisitor().Visit(expression);
return expression;
}
+
+ ///
+ public override Expression ProcessQueryRoots(Expression expression)
+ => new RelationalQueryRootProcessor(RelationalDependencies.RelationalTypeMappingSource, QueryCompilationContext.Model)
+ .Visit(expression);
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs
index a8a505798c4..6bd82bdce6a 100644
--- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs
@@ -45,7 +45,13 @@ public sealed record RelationalQueryTranslationPreprocessorDependencies
/// the constructor at any point in this process.
///
[EntityFrameworkInternal]
- public RelationalQueryTranslationPreprocessorDependencies()
+ public RelationalQueryTranslationPreprocessorDependencies(IRelationalTypeMappingSource relationalTypeMappingSource)
{
+ RelationalTypeMappingSource = relationalTypeMappingSource;
}
+
+ ///
+ /// The type mapping source.
+ ///
+ public IRelationalTypeMappingSource RelationalTypeMappingSource { get; init; }
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 146af8fa44a..5c202f2513e 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -1,8 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
+using System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -16,9 +17,19 @@ public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMe
private readonly SharedTypeEntityExpandingExpressionVisitor _sharedTypeEntityExpandingExpressionVisitor;
private readonly RelationalProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor;
private readonly QueryCompilationContext _queryCompilationContext;
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly bool _subquery;
+ ///
+ /// References all columns in the tree that do not have type mappings set (via their name and the
+ /// they're on. This happens for queryable parameters (e.g. JSON arrays) or constants (VALUES). We attempt to infer the type
+ /// mappings for these columns based on their usage.
+ ///
+ private readonly Dictionary _untypedColumns;
+
+ private ColumnTypeMappingScanner? _columnTypeMappingScanner;
+
///
/// Creates a new instance of the class.
///
@@ -34,12 +45,14 @@ public RelationalQueryableMethodTranslatingExpressionVisitor(
RelationalDependencies = relationalDependencies;
var sqlExpressionFactory = relationalDependencies.SqlExpressionFactory;
+ _typeMappingSource = relationalDependencies.TypeMappingSource;
_queryCompilationContext = queryCompilationContext;
_sqlTranslator = relationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(queryCompilationContext, this);
_sharedTypeEntityExpandingExpressionVisitor =
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory);
_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
_sqlExpressionFactory = sqlExpressionFactory;
+ _untypedColumns = new();
_subquery = false;
}
@@ -64,9 +77,28 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor(
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory);
_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
+ _typeMappingSource = parentVisitor._typeMappingSource;
+ _untypedColumns = parentVisitor._untypedColumns;
_subquery = true;
}
+ ///
+ public override Expression Translate(Expression expression)
+ {
+ var visited = Visit(expression);
+
+ // We've finished translating. If there were any untyped columns (i.e. queryable constants or parameters), we should have collected
+ // their inferred type mappings during SQL translation (see TranslateExpression below). For columns whose type mappings weren't
+ // inferred, get the default based on the CLR type, and apply all these back to the queryable root.
+
+ if (!_subquery)
+ {
+ visited = ProcessTypeMappings(visited, _untypedColumns);
+ }
+
+ return visited;
+ }
+
///
protected override Expression VisitExtension(Expression extensionExpression)
{
@@ -83,6 +115,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
fromSqlQueryRootExpression.Argument)));
case TableValuedFunctionQueryRootExpression tableValuedFunctionQueryRootExpression:
+ {
var function = tableValuedFunctionQueryRootExpression.Function;
var arguments = new List();
foreach (var arg in tableValuedFunctionQueryRootExpression.Arguments)
@@ -122,6 +155,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
var queryExpression = _sqlExpressionFactory.Select(entityType, translation);
return CreateShapedQueryExpression(entityType, queryExpression);
+ }
case EntityQueryRootExpression entityQueryRootExpression
when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
@@ -153,6 +187,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
.Visit(shapedQueryExpression.ShaperExpression));
case SqlQueryRootExpression sqlQueryRootExpression:
+ {
var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType);
if (typeMapping == null)
{
@@ -177,12 +212,30 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
}
return new ShapedQueryExpression(selectExpression, shaperExpression);
+ }
+
+ case ConstantQueryRootExpression constantQueryRootExpression:
+ return VisitConstantPrimitiveCollection(constantQueryRootExpression) ?? base.VisitExtension(extensionExpression);
+
+ case ParameterQueryRootExpression parameterQueryRootExpression:
+ var sqlParameterExpression =
+ _sqlTranslator.Visit(parameterQueryRootExpression.ParameterExpression) as SqlParameterExpression;
+ Check.DebugAssert(sqlParameterExpression is not null, "sqlParameterExpression is not null");
+ return TranslatePrimitiveCollection(sqlParameterExpression) ?? base.VisitExtension(extensionExpression);
default:
return base.VisitExtension(extensionExpression);
}
}
+ ///
+ /// Registers a table expression whose projected type mappings aren't yet known. This is used with table expressions representing
+ /// constants (VALUES) or parameters (e.g. OPENJSON), whose type mappings will get inferred later based on usage.
+ ///
+ /// The table expression not yet typed.
+ protected virtual void RegisterUntypedColumnExpression(TableExpressionBase tableExpression)
+ => _untypedColumns[tableExpression] = null;
+
///
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
@@ -212,7 +265,99 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
}
- return base.VisitMethodCall(methodCallExpression);
+ var translated = base.VisitMethodCall(methodCallExpression);
+
+ if (translated == QueryCompilationContext.NotTranslatedExpression
+ && _sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var propertyAccessExpression)
+ && propertyAccessExpression is ColumnExpression { TypeMapping.ElementTypeMapping: not null }
+ && TranslatePrimitiveCollection(propertyAccessExpression) is { } primitiveCollectionTranslation)
+ {
+ return primitiveCollectionTranslation;
+ }
+
+ return translated;
+ }
+
+ ///
+ /// Translates a primitive collection. Providers can override this to translate e.g. int[] columns/parameters/constants to a
+ /// queryable table (OPENJSON on SQL Server, unnest on PostgreSQL...). The default implementation always
+ /// returns (no translation).
+ ///
+ ///
+ /// See for the translation of constant primitive collections.
+ ///
+ /// The expression to try to translate as a primitive collection expression.
+ /// A if the translation was successful, otherwise .
+ protected virtual ShapedQueryExpression? TranslatePrimitiveCollection(SqlExpression sqlExpression)
+ => null;
+
+ ///
+ /// Translates a constant primitive collection into a queryable SQL VALUES expression.
+ ///
+ /// The constant primitive collection to be translated.
+ /// A queryable SQL VALUES expression.
+ protected virtual ShapedQueryExpression? VisitConstantPrimitiveCollection(ConstantQueryRootExpression constantQueryRootExpression)
+ {
+ var rows = constantQueryRootExpression.ConstantExpression.Value as IEnumerable;
+ Check.DebugAssert(rows is not null, "ConstantQueryRootExpression with non-IEnumerable value");
+ var elementType = constantQueryRootExpression.ElementType;
+
+ var rowExpressions = new List();
+
+ var encounteredNull = false;
+
+ foreach (var row in rows)
+ {
+ if (row is ITuple)
+ {
+ // TODO: Support multi-value rows (tuples)
+ // SelectExpression seems to require actual reflection MemberInfos for its projection mapping, so we'd need to
+ // (in theory) create an anonymous type here. See about changing the design to allow this; in the meantine, support
+ // only single-value row values.
+ throw new NotSupportedException("Tuples aren't supported"); // TODO
+ }
+
+ // TODO: Infer the type mapping
+ // TODO: Construct the RowValueExpression in SqlExpressionFactory, to allow providers to build it with a proper type
+ // mapping?
+ if (row is null)
+ {
+ encounteredNull = true;
+ }
+
+ rowExpressions.Add(
+ new RowValueExpression(
+ new SqlExpression[] { _sqlExpressionFactory.Constant(row, elementType, typeMapping: null) },
+ typeof(ValueTuple
/// A subquery projecting single row with a single scalar projection.
public ScalarSubqueryExpression(SelectExpression subquery)
- : base(Verify(subquery).Projection[0].Type, subquery.Projection[0].Expression.TypeMapping)
+ : this(subquery, subquery.Projection[0].Expression.TypeMapping)
+ {
+ Subquery = subquery;
+ }
+
+ private ScalarSubqueryExpression(SelectExpression subquery, RelationalTypeMapping? typeMapping)
+ : base(Verify(subquery).Projection[0].Type, typeMapping)
{
Subquery = subquery;
}
@@ -46,6 +52,15 @@ private static SelectExpression Verify(SelectExpression selectExpression)
///
public virtual SelectExpression Subquery { get; }
+ ///
+ /// Applies supplied type mapping to this expression.
+ ///
+ /// A relational type mapping to apply.
+ /// A new expression which has supplied type mapping.
+ public SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
+ // TODO: We should also replace the column in the subquery projection, but SelectExpression is too closed for that.
+ => new ScalarSubqueryExpression(Subquery, typeMapping);
+
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
=> Update((SelectExpression)visitor.Visit(Subquery));
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
index ec9dd339dd7..c8e73bc5fa5 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
@@ -604,7 +604,7 @@ public ConcreteColumnExpression(
string name,
TableReferenceExpression table,
Type type,
- RelationalTypeMapping typeMapping,
+ RelationalTypeMapping? typeMapping,
bool nullable)
: base(type, typeMapping)
{
@@ -628,7 +628,10 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
=> this;
public override ConcreteColumnExpression MakeNullable()
- => IsNullable ? this : new ConcreteColumnExpression(Name, _table, Type, TypeMapping!, true);
+ => IsNullable ? this : new ConcreteColumnExpression(Name, _table, Type, TypeMapping, true);
+
+ public override SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
+ => new ConcreteColumnExpression(Name, _table, Type, typeMapping, IsNullable);
public void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect)
=> _table.UpdateTableReference(oldSelect, newSelect);
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index 1c0b5dc22b0..5b08314a294 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -103,7 +103,12 @@ private SelectExpression(string? alias)
{
}
- internal SelectExpression(SqlExpression? projection)
+ ///
+ /// TODO
+ ///
+ /// TODO
+ // TODO: Currently hacked, clean it up
+ public SelectExpression(SqlExpression? projection)
: base(null)
{
if (projection != null)
@@ -112,14 +117,30 @@ internal SelectExpression(SqlExpression? projection)
}
}
- internal SelectExpression(Type type, RelationalTypeMapping typeMapping, FromSqlExpression fromSqlExpression)
+ ///
+ /// TODO
+ ///
+ /// TODO
+ /// TODO
+ /// TODO
+ /// TODO
+ /// TODO
+ // TODO: Currently hacked, clean it up
+ public SelectExpression(
+ Type type,
+ RelationalTypeMapping? typeMapping,
+ TableExpressionBase tableExpression,
+ string? columnName = null,
+ bool? isColumnNullable = null)
: base(null)
{
- var tableReferenceExpression = new TableReferenceExpression(this, fromSqlExpression.Alias!);
- AddTable(fromSqlExpression, tableReferenceExpression);
+ columnName ??= SqlQuerySingleColumnAlias;
+
+ var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!);
+ AddTable(tableExpression, tableReferenceExpression);
var columnExpression = new ConcreteColumnExpression(
- SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType());
+ columnName, tableReferenceExpression, type.UnwrapNullableType(), typeMapping, isColumnNullable ?? type.IsNullableType());
_projectionMapping[new ProjectionMember()] = columnExpression;
}
@@ -831,8 +852,7 @@ public Expression ApplyProjection(
containsCollection = true;
}
- if (sqe.ResultCardinality == ResultCardinality.Single
- || sqe.ResultCardinality == ResultCardinality.SingleOrDefault)
+ if (sqe.ResultCardinality is ResultCardinality.Single or ResultCardinality.SingleOrDefault)
{
containsSingleResult = true;
}
@@ -945,15 +965,15 @@ Expression AddGroupByKeySelectorToProjection(
return memberInitExpression.Update((NewExpression)updatedNewExpression, newBindings);
- case UnaryExpression unaryExpression
- when unaryExpression.NodeType == ExpressionType.Convert
- || unaryExpression.NodeType == ExpressionType.ConvertChecked:
+ case UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression:
return unaryExpression.Update(
AddGroupByKeySelectorToProjection(
selectExpression, clientProjectionList, projectionBindingMap, unaryExpression.Operand));
- case EntityShaperExpression entityShaperExpression
- when entityShaperExpression.ValueBufferExpression is EntityProjectionExpression entityProjectionExpression:
+ case EntityShaperExpression
+ {
+ ValueBufferExpression: EntityProjectionExpression entityProjectionExpression
+ } entityShaperExpression:
{
var clientProjectionToAdd = AddEntityProjection(entityProjectionExpression);
var existingIndex = clientProjectionList.FindIndex(
@@ -1097,9 +1117,10 @@ static void UpdateLimit(SelectExpression selectExpression)
break;
}
- case ShapedQueryExpression shapedQueryExpression
- when shapedQueryExpression.ResultCardinality == ResultCardinality.Single
- || shapedQueryExpression.ResultCardinality == ResultCardinality.SingleOrDefault:
+ case ShapedQueryExpression
+ {
+ ResultCardinality: ResultCardinality.Single or ResultCardinality.SingleOrDefault
+ } shapedQueryExpression:
{
var innerSelectExpression = (SelectExpression)shapedQueryExpression.QueryExpression;
var innerShaperExpression = shapedQueryExpression.ShaperExpression;
@@ -1113,8 +1134,7 @@ static void UpdateLimit(SelectExpression selectExpression)
}
var innerExpression = RemoveConvert(innerShaperExpression);
- if (!(innerExpression is EntityShaperExpression
- || innerExpression is IncludeExpression))
+ if (innerExpression is not EntityShaperExpression and not IncludeExpression)
{
var sentinelExpression = innerSelectExpression.Limit!;
var sentinelNullableType = sentinelExpression.Type.MakeNullable();
@@ -1164,14 +1184,12 @@ static void UpdateLimit(SelectExpression selectExpression)
break;
static Expression RemoveConvert(Expression expression)
- => expression is UnaryExpression unaryExpression
- && unaryExpression.NodeType == ExpressionType.Convert
- ? RemoveConvert(unaryExpression.Operand)
- : expression;
+ => expression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression
+ ? RemoveConvert(unaryExpression.Operand)
+ : expression;
}
- case ShapedQueryExpression shapedQueryExpression
- when shapedQueryExpression.ResultCardinality == ResultCardinality.Enumerable:
+ case ShapedQueryExpression { ResultCardinality: ResultCardinality.Enumerable } shapedQueryExpression:
{
var innerSelectExpression = (SelectExpression)shapedQueryExpression.QueryExpression;
if (_identifier.Count == 0
@@ -1747,9 +1765,7 @@ public void ReplaceProjection(IReadOnlyDictionary
foreach (var (projectionMember, expression) in projectionMapping)
{
Check.DebugAssert(
- expression is SqlExpression
- || expression is EntityProjectionExpression
- || expression is JsonQueryExpression,
+ expression is SqlExpression or EntityProjectionExpression or JsonQueryExpression,
"Invalid operation in the projection.");
_projectionMapping[projectionMember] = expression;
}
@@ -1767,10 +1783,7 @@ public void ReplaceProjection(IReadOnlyList clientProjections)
foreach (var expression in clientProjections)
{
Check.DebugAssert(
- expression is SqlExpression
- || expression is EntityProjectionExpression
- || expression is ShapedQueryExpression
- || expression is JsonQueryExpression,
+ expression is SqlExpression or EntityProjectionExpression or ShapedQueryExpression or JsonQueryExpression,
"Invalid operation in the projection.");
_clientProjections.Add(expression);
_aliasForClientProjections.Add(null);
@@ -2987,8 +3000,7 @@ private void AddJoin(
{
innerPushdownOccurred = false;
// Try to convert Apply to normal join
- if (joinType == JoinType.CrossApply
- || joinType == JoinType.OuterApply)
+ if (joinType is JoinType.CrossApply or JoinType.OuterApply)
{
var limit = innerSelectExpression.Limit;
var offset = innerSelectExpression.Offset;
@@ -3128,8 +3140,7 @@ private void AddJoin(
if (_identifier.Count > 0
&& innerSelectExpression._identifier.Count > 0)
{
- if (joinType == JoinType.LeftJoin
- || joinType == JoinType.OuterApply)
+ if (joinType is JoinType.LeftJoin or JoinType.OuterApply)
{
_identifier.AddRange(innerSelectExpression._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)));
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
index 888e1bbe5ac..648902242aa 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
@@ -35,9 +35,38 @@ private TableValuedFunctionExpression(
IStoreFunction storeFunction,
IReadOnlyList arguments,
IEnumerable? annotations)
+ : this(alias, storeFunction.Name, storeFunction.Schema, storeFunction.IsBuiltIn, arguments, annotations)
+ {
+ _table = storeFunction;
+ Name = storeFunction.Name;
+ Schema = storeFunction.Schema;
+ IsBuiltIn = storeFunction.IsBuiltIn;
+ Arguments = arguments;
+ }
+
+ ///
+ /// TODO
+ ///
+ /// TODO
+ /// TODO
+ /// TODO
+ /// TODO
+ /// TODO
+ /// TODO
+ // TODO: Don't expose alias parameter
+ // TODO: Understand exactly what we need to do with _table
+ public TableValuedFunctionExpression(
+ string alias,
+ string name,
+ string? schema,
+ bool builtIn,
+ IReadOnlyList arguments,
+ IEnumerable? annotations = null)
: base(alias, annotations)
{
- StoreFunction = storeFunction;
+ Name = name;
+ Schema = schema;
+ IsBuiltIn = builtIn;
Arguments = arguments;
}
@@ -52,18 +81,33 @@ public override string? Alias
}
///
- /// The store function.
+ /// The name of the table or view.
///
- public virtual IStoreFunction StoreFunction { get; }
+ public string Name { get; }
+
+ ///
+ /// The schema of the table or view.
+ ///
+ public string? Schema { get; }
+
+ ///
+ /// Gets the value indicating whether the database function is built-in.
+ ///
+ public bool IsBuiltIn { get; }
///
/// The list of arguments of this function.
///
public virtual IReadOnlyList Arguments { get; }
+ // TODO. The Table is only actually needed when this is the first table in a SelectExpression for an entity type; this is never our case.
///
- ITableBase ITableBasedExpression.Table
- => StoreFunction;
+ ITableBase ITableBasedExpression.Table => _table!;
+
+ private readonly ITableBase? _table;
+
+ // ITableBase ITableBasedExpression.Table
+ // => StoreFunction;
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
@@ -77,7 +121,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
}
return changed
- ? new TableValuedFunctionExpression(Alias, StoreFunction, arguments, GetAnnotations())
+ ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations())
: this;
}
@@ -89,22 +133,22 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// This expression if no children changed, or an expression with the updated children.
public virtual TableValuedFunctionExpression Update(IReadOnlyList arguments)
=> !arguments.SequenceEqual(Arguments)
- ? new TableValuedFunctionExpression(Alias, StoreFunction, arguments, GetAnnotations())
+ ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations())
: this;
///
protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations)
- => new TableValuedFunctionExpression(Alias, StoreFunction, Arguments, annotations);
+ => new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, Arguments, annotations);
///
protected override void Print(ExpressionPrinter expressionPrinter)
{
- if (!string.IsNullOrEmpty(StoreFunction.Schema))
+ if (!string.IsNullOrEmpty(Schema))
{
- expressionPrinter.Append(StoreFunction.Schema).Append(".");
+ expressionPrinter.Append(Schema).Append(".");
}
- expressionPrinter.Append(StoreFunction.Name);
+ expressionPrinter.Append(Name);
expressionPrinter.Append("(");
expressionPrinter.VisitCollection(Arguments);
expressionPrinter.Append(")");
@@ -122,15 +166,19 @@ public override bool Equals(object? obj)
private bool Equals(TableValuedFunctionExpression tableValuedFunctionExpression)
=> base.Equals(tableValuedFunctionExpression)
- && StoreFunction == tableValuedFunctionExpression.StoreFunction
- && Arguments.SequenceEqual(tableValuedFunctionExpression.Arguments);
+ && Name == tableValuedFunctionExpression.Name
+ && Schema == tableValuedFunctionExpression.Schema
+ && IsBuiltIn == tableValuedFunctionExpression.IsBuiltIn
+ && Arguments.SequenceEqual(tableValuedFunctionExpression.Arguments);
///
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(base.GetHashCode());
- hash.Add(StoreFunction);
+ hash.Add(Name);
+ hash.Add(Schema);
+ hash.Add(IsBuiltIn);
for (var i = 0; i < Arguments.Count; i++)
{
hash.Add(Arguments[i]);
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
new file mode 100644
index 00000000000..0edaa4294e5
--- /dev/null
+++ b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
@@ -0,0 +1,178 @@
+// 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 constant table in SQL, sometimes known as a table value constructor.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class ValuesExpression : TableExpressionBase
+{
+ ///
+ /// The row values for this table.
+ ///
+ public virtual IReadOnlyList RowValues { get; }
+
+ ///
+ /// The names of the columns contained in this table.
+ ///
+ public virtual IReadOnlyList ColumnNames { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// The row values for this table.
+ /// The names of the columns contained in this table.
+ /// A collection of annotations associated with this expression.
+ public ValuesExpression(
+ string? alias,
+ IReadOnlyList rowValues,
+ IReadOnlyList columnNames,
+ IEnumerable? annotations = null)
+ : base(alias, annotations)
+ {
+ Check.NotEmpty(rowValues, nameof(rowValues));
+
+#if DEBUG
+ if (rowValues.Any(rv => rv.Values.Count != columnNames.Count))
+ {
+ throw new ArgumentException("All number of all row values doesn't match the number of column names");
+ }
+
+ if (rowValues.SelectMany(rv => rv.Values).Any(
+ v => v is not SqlConstantExpression and not SqlUnaryExpression
+ {
+ Operand: SqlConstantExpression, OperatorType: ExpressionType.Convert
+ }))
+ {
+ throw new ArgumentException("Only constant expressions are supported in ValuesExpression");
+ }
+#endif
+
+ RowValues = rowValues;
+ ColumnNames = columnNames;
+ }
+
+ ///
+ /// The alias assigned to this table source.
+ ///
+ [NotNull]
+ public override string? Alias
+ {
+ get => base.Alias!;
+ internal set => base.Alias = value;
+ }
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ Check.NotNull(visitor, nameof(visitor));
+
+ RowValueExpression[]? newRowValues = null;
+
+ for (var i = 0; i < RowValues.Count; i++)
+ {
+ var rowValue = RowValues[i];
+ var visited = (RowValueExpression)visitor.Visit(rowValue);
+ if (visited != rowValue && newRowValues is null)
+ {
+ newRowValues = new RowValueExpression[RowValues.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newRowValues[j] = RowValues[j];
+ }
+ }
+
+ if (newRowValues is not null)
+ {
+ newRowValues[i] = visited;
+ }
+ }
+
+ return newRowValues is null ? this : new ValuesExpression(Alias, newRowValues, ColumnNames);
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual ValuesExpression Update(IReadOnlyList rowValues)
+ => rowValues.Count == RowValues.Count && rowValues.Zip(RowValues, (x, y) => (x, y)).All(tup => tup.x == tup.y)
+ ? this
+ : new ValuesExpression(Alias, rowValues, ColumnNames);
+
+ ///
+ protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations)
+ => new ValuesExpression(Alias, RowValues, ColumnNames, annotations);
+
+ ///
+ /// Creates a printable string representation of the given expression using .
+ ///
+ /// The expression printer to use.
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("VALUES (");
+
+ var count = RowValues.Count;
+ for (var i = 0; i < count; i++)
+ {
+ expressionPrinter.Visit(RowValues[i]);
+
+ if (i < count - 1)
+ {
+ expressionPrinter.Append(", ");
+ }
+ }
+
+ expressionPrinter.Append(")");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is ValuesExpression other && Equals(other);
+
+ private bool Equals(ValuesExpression? other)
+ {
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ if (other is null || !base.Equals(other) || other.RowValues.Count != RowValues.Count)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < RowValues.Count; i++)
+ {
+ if (!other.RowValues[i].Equals(RowValues[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+
+ foreach (var rowValue in RowValues)
+ {
+ hashCode.Add(rowValue);
+ }
+
+ return hashCode.ToHashCode();
+ }
+}
diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
index 1b413d0faa2..4bf770e9105 100644
--- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
+++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
@@ -185,6 +185,18 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB
case OuterApplyExpression outerApplyExpression:
return outerApplyExpression.Update(Visit(outerApplyExpression.Table));
+ case ValuesExpression valuesExpression:
+ {
+ // TODO: Optimize to not allocate if nothing changes, also TableValuedFunctionExpression below
+ var rowValues = new List();
+ foreach (var rowValue in valuesExpression.RowValues)
+ {
+ rowValues.Add((RowValueExpression)VisitRowValue(rowValue, allowOptimizedExpansion: false, out _));
+ }
+
+ return valuesExpression.Update(rowValues);
+ }
+
case SelectExpression selectExpression:
return Visit(selectExpression);
@@ -403,6 +415,8 @@ LikeExpression likeExpression
=> VisitLike(likeExpression, allowOptimizedExpansion, out nullable),
RowNumberExpression rowNumberExpression
=> VisitRowNumber(rowNumberExpression, allowOptimizedExpansion, out nullable),
+ RowValueExpression rowValueExpression
+ => VisitRowValue(rowValueExpression, allowOptimizedExpansion, out nullable),
ScalarSubqueryExpression scalarSubqueryExpression
=> VisitScalarSubquery(scalarSubqueryExpression, allowOptimizedExpansion, out nullable),
SqlBinaryExpression sqlBinaryExpression
@@ -605,7 +619,7 @@ protected virtual SqlExpression VisitExists(
nullable = false;
// if subquery has predicate which evaluates to false, we can simply return false
- // if the exisits is negated we need to return true instead
+ // if the exists is negated we need to return true instead
return TryGetBoolConstantValue(subquery.Predicate) == false
? _sqlExpressionFactory.Constant(existsExpression.IsNegated, existsExpression.TypeMapping)
: existsExpression.Update(subquery);
@@ -826,6 +840,47 @@ protected virtual SqlExpression VisitRowNumber(
: rowNumberExpression;
}
+ ///
+ /// Visits a and computes its nullability.
+ ///
+ /// A row value expression to visit.
+ /// A bool value indicating if optimized expansion which considers null value as false value is allowed.
+ /// A bool value indicating whether the sql expression is nullable.
+ /// An optimized sql expression.
+ protected virtual SqlExpression VisitRowValue(
+ RowValueExpression rowValueExpression,
+ bool allowOptimizedExpansion,
+ out bool nullable)
+ {
+ SqlExpression[]? newValues = null;
+
+ for (var i = 0; i < rowValueExpression.Values.Count; i++)
+ {
+ var value = rowValueExpression.Values[i];
+
+ // Note that we disallow optimized expansion, since the null vs. false distinction does matter inside the row's values
+ var newValue = Visit(value, allowOptimizedExpansion: false, out _);
+ if (newValue != value && newValues is null)
+ {
+ newValues = new SqlExpression[rowValueExpression.Values.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newValues[j] = rowValueExpression.Values[j];
+ }
+ }
+
+ if (newValues is not null)
+ {
+ newValues[i] = newValue;
+ }
+ }
+
+ // The row value expression itself can never be null
+ nullable = false;
+
+ return rowValueExpression.Update(newValues ?? rowValueExpression.Values);
+ }
+
///
/// Visits a and computes its nullability.
///
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
index a994c313168..d1d5024d23b 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
@@ -126,6 +126,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd(p => p.GetRequiredService())
.TryAddProviderSpecificServices(
diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
index 3e0407691a0..a093a3f101e 100644
--- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
@@ -608,6 +608,15 @@ protected override Expression VisitInnerJoin(InnerJoinExpression innerJoinExpres
return innerJoinExpression.Update(table, joinPredicate);
}
+ ///
+ /// 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 VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
+ => ApplyConversion(jsonScalarExpression, condition: false);
+
///
/// 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
@@ -670,6 +679,27 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
return ApplyConversion(rowNumberExpression.Update(partitions, orderings), condition: false);
}
+ ///
+ /// 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 VisitRowValue(RowValueExpression rowValueExpression)
+ {
+ var parentSearchCondition = _isSearchCondition;
+ _isSearchCondition = false;
+
+ var values = new SqlExpression[rowValueExpression.Values.Count];
+ for (var i = 0; i < values.Length; i++)
+ {
+ values[i] = (SqlExpression)Visit(rowValueExpression.Values[i]);
+ }
+
+ _isSearchCondition = parentSearchCondition;
+ return rowValueExpression.Update(values);
+ }
+
///
/// 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
@@ -763,6 +793,18 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
/// 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 VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
- => ApplyConversion(jsonScalarExpression, condition: false);
+ protected override Expression VisitValues(ValuesExpression valuesExpression)
+ {
+ var parentSearchCondition = _isSearchCondition;
+ _isSearchCondition = false;
+
+ var rowValues = new RowValueExpression[valuesExpression.RowValues.Count];
+ for (var i = 0; i < rowValues.Length; i++)
+ {
+ rowValues[i] = (RowValueExpression)Visit(valuesExpression.RowValues[i]);
+ }
+
+ _isSearchCondition = parentSearchCondition;
+ return valuesExpression.Update(rowValues);
+ }
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs
index 0e7698527cd..cfedfccc353 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs
@@ -11,7 +11,8 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.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 SqlServerNavigationExpansionExtensibilityHelper : NavigationExpansionExtensibilityHelper
+public class SqlServerNavigationExpansionExtensibilityHelper
+ : NavigationExpansionExtensibilityHelper, INavigationExpansionExtensibilityHelper
{
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
new file mode 100644
index 00000000000..43b1709576b
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
@@ -0,0 +1,150 @@
+// 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.Query.SqlExpressions;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
+
+///
+/// An expression that represents a SQL Server OPENJSON function call in a SQL tree.
+///
+///
+///
+/// See OPENJSON (Transact-SQL) for more
+/// information and examples.
+///
+///
+/// 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 class SqlServerOpenJsonExpression : TableValuedFunctionExpression
+{
+ ///
+ /// 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 SqlExpression JsonExpression
+ => Arguments[0];
+
+ ///
+ /// 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 SqlExpression? Path
+ => Arguments.Count == 1 ? null : Arguments[1];
+
+ ///
+ /// 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 IReadOnlyList? ColumnInfos { 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 SqlServerOpenJsonExpression(
+ SqlExpression jsonExpression,
+ SqlExpression? path = null,
+ IReadOnlyList? columnInfos = null)
+ : this("j", jsonExpression, path, columnInfos)
+ {
+ }
+
+ ///
+ /// 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 SqlServerOpenJsonExpression(
+ string alias,
+ SqlExpression jsonExpression,
+ SqlExpression? path = null,
+ IReadOnlyList? columnInfos = null)
+ : base(alias, "OpenJson", schema: null, builtIn: true, path is null ? new[] { jsonExpression } : new[] { jsonExpression, path })
+ {
+ ColumnInfos = columnInfos;
+ }
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append(Name);
+ expressionPrinter.Append("(");
+ expressionPrinter.VisitCollection(Arguments);
+ expressionPrinter.Append(")");
+
+ if (ColumnInfos is not null)
+ {
+ expressionPrinter.Append(" WITH (");
+
+ for (var i = 0; i < ColumnInfos.Count; i++)
+ {
+ var columnInfo = ColumnInfos[i];
+
+ if (i > 0)
+ {
+ expressionPrinter.Append(", ");
+ }
+
+ expressionPrinter
+ .Append(columnInfo.Name)
+ .Append(" ")
+ .Append(columnInfo.StoreType ?? "");
+
+ if (columnInfo.Path is not null)
+ {
+ expressionPrinter.Append(" ").Append("'" + columnInfo.Path + "'");
+ }
+
+ if (columnInfo.AsJson)
+ {
+ expressionPrinter.Append(" AS JSON");
+ }
+ }
+
+ expressionPrinter.Append(")");
+ }
+
+ PrintAnnotations(expressionPrinter);
+ expressionPrinter.Append(" AS ");
+ expressionPrinter.Append(Alias);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj != null
+ && (ReferenceEquals(this, obj)
+ || obj is SqlServerOpenJsonExpression openJsonExpression
+ && Equals(openJsonExpression));
+
+ private bool Equals(SqlServerOpenJsonExpression openJsonExpression)
+ => base.Equals(openJsonExpression)
+ && (ColumnInfos is null
+ ? openJsonExpression.ColumnInfos is null
+ : openJsonExpression.ColumnInfos is not null && ColumnInfos.SequenceEqual(openJsonExpression.ColumnInfos));
+
+ ///
+ public override int GetHashCode()
+ => base.GetHashCode();
+
+ ///
+ /// 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 readonly record struct ColumnInfo(string Name, string? StoreType, string? Path = null, bool AsJson = false);
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index 3f7fd0bc9eb..6d791844f25 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
public class SqlServerQuerySqlGenerator : QuerySqlGenerator
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlGenerationHelper _sqlGenerationHelper;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -29,8 +30,22 @@ public SqlServerQuerySqlGenerator(
: base(dependencies)
{
_typeMappingSource = typeMappingSource;
+ _sqlGenerationHelper = dependencies.SqlGenerationHelper;
}
+ ///
+ /// 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 bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
+ // SQL Server doesn't support VALUES as a top-level statement, so we need to wrap the VALUES in a SELECT:
+ // SELECT 1 UNION VALUES (2), (3) -- simple
+ // SELECT 1 AS x UNION SELECT * FROM (VALUES (2), (3)) AS f(x) -- SQL Server
+ => selectExpression.Tables is not [ValuesExpression]
+ && base.TryGenerateWithoutWrappingSelect(selectExpression);
+
///
/// 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
@@ -138,6 +153,65 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
}
+ ///
+ /// 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 VisitValues(ValuesExpression valuesExpression)
+ {
+ base.VisitValues(valuesExpression);
+
+ // SQL Server VALUES supports setting the projects column names: FROM (VALUES (1), (2)) AS v(foo)
+ Sql.Append("(");
+
+ for (var i = 0; i < valuesExpression.ColumnNames.Count; i++)
+ {
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Sql.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
+ }
+
+ Sql.Append(")");
+
+ return valuesExpression;
+ }
+
+ ///
+ /// 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 GenerateValues(ValuesExpression valuesExpression)
+ {
+ // Unlike other database, SQL Server doesn't support top-level VALUES
+ Check.DebugAssert(valuesExpression.Alias is not null, "ValuesExpression without alias");
+
+ // SQL Server supports providing the names of columns projected out of VALUES: (VALUES (1, 3), (2, 4)) AS x(a, b).
+ // But since other databases sometimes don't, the default relational implementation is complex, involving a SELECT for the first row
+ // and a UNION All on the rest. Override to do the nice simple thing.
+
+ var rowValues = valuesExpression.RowValues;
+
+ Sql.Append("VALUES ");
+
+ for (var i = 0; i < rowValues.Count; i++)
+ {
+ // TODO: Do we want newlines here?
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Visit(valuesExpression.RowValues[i]);
+ }
+ }
+
///
/// 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
@@ -305,6 +379,9 @@ when tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationTy
case SqlServerAggregateFunctionExpression aggregateFunctionExpression:
return VisitSqlServerAggregateFunction(aggregateFunctionExpression);
+
+ case SqlServerOpenJsonExpression openJsonExpression:
+ return VisitOpenJsonExpression(openJsonExpression);
}
return base.VisitExtension(extensionExpression);
@@ -381,6 +458,65 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
return jsonScalarExpression;
}
+ ///
+ /// 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 virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression openJsonExpression)
+ {
+ // OPENJSON docs: https://learn.microsoft.com/sql/t-sql/functions/openjson-transact-sql
+
+ // OPENJSON is a regular table-valued function with a special WITH clause at the end
+ // Copy-paste from VisitTableValuedFunction, because that appends the 'AS ' but we need to insert WITH before that
+ Sql.Append("OpenJson(");
+
+ GenerateList(openJsonExpression.Arguments, e => Visit(e));
+
+ Sql.Append(")");
+
+ if (openJsonExpression.ColumnInfos is not null)
+ {
+ Sql.Append(" WITH (");
+
+ for (var i = 0; i < openJsonExpression.ColumnInfos.Count; i++)
+ {
+ var columnInfo = openJsonExpression.ColumnInfos[i];
+
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Check.DebugAssert(columnInfo.StoreType is not null, "Unset OpenJson column store type");
+
+ Sql
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnInfo.Name))
+ .Append(" ")
+ .Append(columnInfo.StoreType);
+
+ if (columnInfo.Path is not null)
+ {
+ Sql
+ .Append(" ")
+ .Append(_typeMappingSource.GetMapping("varchar(max)").GenerateSqlLiteral(columnInfo.Path));
+ }
+
+ if (columnInfo.AsJson)
+ {
+ Sql.Append(" AS JSON");
+ }
+ }
+
+ Sql.Append(")");
+ }
+
+ Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(openJsonExpression.Alias));
+
+ return openJsonExpression;
+ }
+
///
protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql)
{
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs
new file mode 100644
index 00000000000..5dc9076321a
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs
@@ -0,0 +1,37 @@
+// 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.SqlServer.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 class SqlServerQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
+{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// Parameter object containing dependencies for this class.
+ /// Parameter object containing relational dependencies for this class.
+ /// The query compilation context object to use.
+ public SqlServerQueryTranslationPreprocessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ {
+ }
+
+ ///
+ /// 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 ProcessQueryRoots(Expression expression)
+ => new SqlServerQueryRootProcessor(RelationalDependencies.RelationalTypeMappingSource, QueryCompilationContext.Model)
+ .Visit(expression);
+}
diff --git a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs
similarity index 53%
rename from src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs
rename to src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs
index 7a433e8c5c5..c113445bab7 100644
--- a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs
@@ -1,7 +1,7 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// 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;
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -9,9 +9,9 @@ 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 TableValuedFunctionToQueryRootConvertingExpressionVisitor : ExpressionVisitor
+public class SqlServerQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
{
- private readonly IModel _model;
+ private readonly ITypeMappingSource _typeMappingSource;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -19,29 +19,32 @@ public class TableValuedFunctionToQueryRootConvertingExpressionVisitor : Express
/// 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 TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model)
+ public SqlServerQueryTranslationPreprocessorFactory(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ ITypeMappingSource typeMappingSource)
{
- _model = model;
+ Dependencies = dependencies;
+ RelationalDependencies = relationalDependencies;
+ _typeMappingSource = typeMappingSource;
}
+ ///
+ /// Dependencies for this service.
+ ///
+ protected virtual QueryTranslationPreprocessorDependencies Dependencies { get; }
+
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { 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.
///
- protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
- {
- var function = _model.FindDbFunction(methodCallExpression.Method);
-
- return function?.IsScalar == false
- ? CreateTableValuedFunctionQueryRootExpression(function.StoreFunction, methodCallExpression.Arguments)
- : base.VisitMethodCall(methodCallExpression);
- }
-
- private static Expression CreateTableValuedFunctionQueryRootExpression(
- IStoreFunction function,
- IReadOnlyCollection arguments)
- // See issue #19970
- => new TableValuedFunctionQueryRootExpression(function.EntityTypeMappings.Single().EntityType, function, arguments);
+ public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
+ => new SqlServerQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext);
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index ea225751457..d849bd0927b 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
@@ -15,6 +16,10 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
+ private readonly QueryCompilationContext _queryCompilationContext;
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
///
/// 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
@@ -27,6 +32,9 @@ public SqlServerQueryableMethodTranslatingExpressionVisitor(
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
+ _queryCompilationContext = queryCompilationContext;
+ _typeMappingSource = relationalDependencies.TypeMappingSource;
+ _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory;
}
///
@@ -39,6 +47,9 @@ protected SqlServerQueryableMethodTranslatingExpressionVisitor(
SqlServerQueryableMethodTranslatingExpressionVisitor parentVisitor)
: base(parentVisitor)
{
+ _queryCompilationContext = parentVisitor._queryCompilationContext;
+ _typeMappingSource = parentVisitor._typeMappingSource;
+ _sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
}
///
@@ -58,48 +69,128 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
///
protected override Expression VisitExtension(Expression extensionExpression)
{
- if (extensionExpression is TemporalQueryRootExpression queryRootExpression)
+ switch (extensionExpression)
{
- var selectExpression = RelationalDependencies.SqlExpressionFactory.Select(queryRootExpression.EntityType);
- Func annotationApplyingFunc = queryRootExpression switch
+ case TemporalQueryRootExpression queryRootExpression:
{
- TemporalAllQueryRootExpression => te => te
- .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.All),
- TemporalAsOfQueryRootExpression asOf => te => te
- .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.AsOf)
- .AddAnnotation(SqlServerAnnotationNames.TemporalAsOfPointInTime, asOf.PointInTime),
- TemporalBetweenQueryRootExpression between => te => te
- .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.Between)
- .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, between.From)
- .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, between.To),
- TemporalContainedInQueryRootExpression containedIn => te => te
- .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.ContainedIn)
- .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, containedIn.From)
- .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, containedIn.To),
- TemporalFromToQueryRootExpression fromTo => te => te
- .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.FromTo)
- .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, fromTo.From)
- .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, fromTo.To),
- _ => throw new InvalidOperationException(queryRootExpression.Print()),
- };
+ var selectExpression = RelationalDependencies.SqlExpressionFactory.Select(queryRootExpression.EntityType);
+ Func annotationApplyingFunc = queryRootExpression switch
+ {
+ TemporalAllQueryRootExpression => te => te
+ .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.All),
+ TemporalAsOfQueryRootExpression asOf => te => te
+ .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.AsOf)
+ .AddAnnotation(SqlServerAnnotationNames.TemporalAsOfPointInTime, asOf.PointInTime),
+ TemporalBetweenQueryRootExpression between => te => te
+ .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.Between)
+ .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, between.From)
+ .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, between.To),
+ TemporalContainedInQueryRootExpression containedIn => te => te
+ .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.ContainedIn)
+ .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, containedIn.From)
+ .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, containedIn.To),
+ TemporalFromToQueryRootExpression fromTo => te => te
+ .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.FromTo)
+ .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, fromTo.From)
+ .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, fromTo.To),
+ _ => throw new InvalidOperationException(queryRootExpression.Print()),
+ };
+
+ selectExpression = (SelectExpression)new TemporalAnnotationApplyingExpressionVisitor(annotationApplyingFunc)
+ .Visit(selectExpression);
+
+ return new ShapedQueryExpression(
+ selectExpression,
+ new RelationalEntityShaperExpression(
+ queryRootExpression.EntityType,
+ new ProjectionBindingExpression(
+ selectExpression,
+ new ProjectionMember(),
+ typeof(ValueBuffer)),
+ false));
+ }
+
+ default:
+ return base.VisitExtension(extensionExpression);
+ }
+ }
+
+ ///
+ /// 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 ShapedQueryExpression? TranslatePrimitiveCollection(SqlExpression sqlExpression)
+ {
+ var elementClrType = sqlExpression.Type.GetSequenceType();
+ RelationalTypeMapping? elementTypeMapping = null;
+
+ switch (sqlExpression)
+ {
+ case ColumnExpression
+ {
+ TypeMapping: SqlServerStringTypeMapping
+ {
+ Converter: CollectionToJsonStringConverter,
+ ElementTypeMapping: RelationalTypeMapping e
+ }
+ }:
+ elementTypeMapping = e;
+ break;
+
+ case SqlParameterExpression parameterExpression:
+ // TODO: Hack until I finish proper type mapping inference on the parameter. We should pass null here as the parameter's
+ // type mapping, to allow it to properly get inferred later.
+ // After that, simplify this whole block.
+ sqlExpression = parameterExpression.ApplyTypeMapping(
+ _typeMappingSource.FindMapping(parameterExpression.Type));
+
+ break;
+
+ default:
+ return null;
+ }
+
+ var openJsonExpression = new SqlServerOpenJsonExpression(
+ sqlExpression,
+ columnInfos: new[] { new SqlServerOpenJsonExpression.ColumnInfo("Value", elementTypeMapping?.StoreType, "$") });
+
+ // TODO: Probably move this up to relational...
+ if (elementTypeMapping is null)
+ {
+ RegisterUntypedColumnExpression(openJsonExpression);
+ }
+
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression
+ var selectExpression = new SelectExpression(elementClrType, elementTypeMapping, openJsonExpression);
- selectExpression = (SelectExpression)new TemporalAnnotationApplyingExpressionVisitor(annotationApplyingFunc)
- .Visit(selectExpression);
-
- return new ShapedQueryExpression(
- selectExpression,
- new RelationalEntityShaperExpression(
- queryRootExpression.EntityType,
- new ProjectionBindingExpression(
- selectExpression,
- new ProjectionMember(),
- typeof(ValueBuffer)),
- false));
+ Expression shaperExpression = new ProjectionBindingExpression(
+ selectExpression, new ProjectionMember(), elementClrType.MakeNullable());
+
+ if (elementClrType != shaperExpression.Type)
+ {
+ Check.DebugAssert(
+ elementClrType.MakeNullable() == shaperExpression.Type,
+ "expression.Type must be nullable of targetType");
+
+ shaperExpression = Expression.Convert(shaperExpression, elementClrType);
}
- return base.VisitExtension(extensionExpression);
+ return new ShapedQueryExpression(selectExpression, shaperExpression);
}
+ ///
+ /// 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 ProcessTypeMappings(
+ Expression expression,
+ Dictionary inferredTypeMappings)
+ => new SqlServerTypeMappingProcessor(inferredTypeMappings).Visit(expression);
+
///
/// 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
@@ -206,4 +297,67 @@ public TemporalAnnotationApplyingExpressionVisitor(Func
+ /// 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 class SqlServerTypeMappingProcessor : TypeMappingProcessor
+ {
+ ///
+ /// 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 SqlServerTypeMappingProcessor(
+ Dictionary inferredTypeMappings)
+ : base(inferredTypeMappings)
+ {
+ }
+
+ ///
+ /// 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 VisitExtension(Expression expression)
+ => expression switch
+ {
+ SqlServerOpenJsonExpression openJsonExpression
+ when InferredTypeMappings.TryGetValue(openJsonExpression, out var typeMapping)
+ && typeMapping is not null
+ => ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping }),
+
+ _ => base.VisitExtension(expression)
+ };
+
+ ///
+ /// 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 SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(
+ SqlServerOpenJsonExpression openJsonExpression,
+ IReadOnlyList typeMappings)
+ {
+ Check.DebugAssert(openJsonExpression.ColumnInfos is not null, "ColumnInfos is not null");
+
+ // TODO: multiple column support (tuples)
+ Check.DebugAssert(openJsonExpression.ColumnInfos.Count == 1, "ColumnInfos.Count == 1");
+ var oldColumnInfo = openJsonExpression.ColumnInfos[0];
+
+ Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1");
+
+ return new SqlServerOpenJsonExpression(
+ openJsonExpression.Alias,
+ openJsonExpression.JsonExpression,
+ openJsonExpression.Path,
+ new[] { new SqlServerOpenJsonExpression.ColumnInfo(oldColumnInfo.Name, typeMappings[0].StoreType, oldColumnInfo.Path) });
+ }
+ }
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs
index 47185ef34ac..b9bc102c7ed 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs
@@ -37,21 +37,18 @@ public SqlServerSqlExpressionFactory(SqlExpressionFactoryDependencies dependenci
[return: NotNullIfNotNull("sqlExpression")]
public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping)
{
-#pragma warning disable IDE0046 // Convert to conditional expression
- if (sqlExpression == null
-#pragma warning restore IDE0046 // Convert to conditional expression
- || sqlExpression.TypeMapping != null)
+ if (sqlExpression is { TypeMapping: null })
{
- return sqlExpression;
- }
+ return sqlExpression switch
+ {
+ AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping),
+ SqlServerAggregateFunctionExpression e => e.ApplyTypeMapping(typeMapping),
- return sqlExpression switch
- {
- AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping),
- SqlServerAggregateFunctionExpression e => e.ApplyTypeMapping(typeMapping),
+ _ => base.ApplyTypeMapping(sqlExpression, typeMapping)
+ };
+ }
- _ => base.ApplyTypeMapping(sqlExpression, typeMapping)
- };
+ return base.ApplyTypeMapping(sqlExpression, typeMapping);
}
private SqlExpression ApplyTypeMappingOnAtTimeZone(AtTimeZoneExpression atTimeZoneExpression, RelationalTypeMapping? typeMapping)
diff --git a/src/EFCore.SqlServer/Query/SqlServerQueryRootProcessor.cs b/src/EFCore.SqlServer/Query/SqlServerQueryRootProcessor.cs
new file mode 100644
index 00000000000..6c08a7afae0
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/SqlServerQueryRootProcessor.cs
@@ -0,0 +1,35 @@
+// 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.SqlServer.Query;
+
+///
+public class SqlServerQueryRootProcessor : RelationalQueryRootProcessor
+{
+ private readonly ITypeMappingSource _typeMappingSource;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The type mapping source.
+ /// The model.
+ public SqlServerQueryRootProcessor(ITypeMappingSource typeMappingSource, IModel model)
+ : base(typeMappingSource, model)
+ => _typeMappingSource = typeMappingSource;
+
+ // ///
+ // protected override Expression VisitQueryableParameter(ParameterExpression parameterExpression)
+ // {
+ // // TODO: Also, maybe this type checking should be in the base class.
+ // // TODO: don't convert anything if we're on an old SQL Server version.
+ // // SQL Server's OpenJson, which we use to unpack the queryable parameter, does not support geometry (or any other non-built-in
+ // // types)
+ //
+ // // We convert to query roots only parameters which are enumerable, and for which we have a type mapping converting them to a JSON
+ // // string. This ensures we only create query roots which can be transferred to SQL Server via OPENJSON.
+ // return parameterExpression.Type.TryGetSequenceType() is Type elementType
+ // && _typeMappingSource.FindMapping(parameterExpression.Type) is { Converter: IJsonStringConverter }
+ // ? new ParameterQueryRootExpression(elementType, parameterExpression)
+ // : parameterExpression;
+ // }
+}
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
index 8d0d053cd62..d96b7852c5b 100644
--- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
+++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
@@ -125,6 +125,11 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
/// 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 RelationalTypeMapping CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping)
+ => new SqlServerStringTypeMapping(
+ Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)),
+ _sqlDbType);
+
protected override void ConfigureParameter(DbParameter parameter)
{
var value = parameter.Value;
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
index 21f5884a764..2e7f1cc84e7 100644
--- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
+++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
@@ -4,6 +4,7 @@
using System.Collections;
using System.Data;
using System.Text.Json;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
@@ -175,6 +176,8 @@ private readonly SqlServerJsonTypeMapping _json
private readonly Dictionary _storeTypeMappings;
+ private readonly bool _supportsOpenJson;
+
///
/// 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
@@ -183,7 +186,8 @@ private readonly SqlServerJsonTypeMapping _json
///
public SqlServerTypeMappingSource(
TypeMappingSourceDependencies dependencies,
- RelationalTypeMappingSourceDependencies relationalDependencies)
+ RelationalTypeMappingSourceDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
: base(dependencies, relationalDependencies)
{
_clrTypeMappings
@@ -270,6 +274,8 @@ public SqlServerTypeMappingSource(
{ "xml", new[] { _xml } }
};
// ReSharper restore CoVariantArrayConversion
+
+ _supportsOpenJson = sqlServerSingletonOptions.CompatibilityLevel >= 130;
}
///
@@ -279,7 +285,9 @@ public SqlServerTypeMappingSource(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
- => base.FindMapping(mappingInfo) ?? FindRawMapping(mappingInfo)?.Clone(mappingInfo);
+ => base.FindMapping(mappingInfo)
+ ?? FindRawMapping(mappingInfo)?.Clone(mappingInfo)
+ ?? FindCollectionMapping(mappingInfo)?.Clone(mappingInfo);
private RelationalTypeMapping? FindRawMapping(RelationalTypeMappingInfo mappingInfo)
{
@@ -398,7 +406,7 @@ public SqlServerTypeMappingSource(
var isFixedLength = mappingInfo.IsFixedLength == true;
var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? 900 : null);
- if (size < 0 || size > 8000)
+ if (size is < 0 or > 8000)
{
size = isFixedLength ? 8000 : null;
}
@@ -415,6 +423,82 @@ public SqlServerTypeMappingSource(
return null;
}
+ private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo)
+ {
+ // Support mapping to a JSON array when the following is satisfied:
+ // 1. The ClrType is an array.
+ // 2. The store type is either not given or a string type.
+ // 3. The element CLR type has a type mapping.
+
+ // TODO: We currently support mapping any IList<>, do we want to support HashSet? Any collection? Compare to our policy for
+ // TODO: regular navigations, but remember that here we have order.
+ if (mappingInfo.ClrType?.TryGetElementType(typeof(IList<>)) is not { } elementClrType)
+ {
+ return null;
+ }
+
+ switch (mappingInfo.StoreTypeNameBase)
+ {
+ case "char varying":
+ case "char":
+ case "character varying":
+ case "character":
+ case "national char varying":
+ case "national character varying":
+ case "national character":
+ case "varchar":
+ case null:
+ break;
+ default:
+ return null;
+ }
+
+ // TODO: need to allow the user to set the element store type
+
+ if (FindMapping(elementClrType) is not { } elementTypeMapping)
+ {
+ return null;
+ }
+
+ // Specifically exclude collections over Geometry, since there's a dedicated GeometryCollection type for that (see #30630)
+ if (elementClrType.Namespace == "NetTopologySuite.Geometries")
+ {
+ return null;
+ }
+
+ // TODO: This can be moved into a SQL Server implementation of ValueConverterSelector.. But it seems better for this method's logic
+ // to be in the type mapping source.
+ var stringMappingInfo = new RelationalTypeMappingInfo(
+ typeof(string),
+ mappingInfo.StoreTypeName,
+ mappingInfo.StoreTypeNameBase,
+ mappingInfo.IsKeyOrIndex,
+ mappingInfo.IsUnicode,
+ mappingInfo.Size,
+ mappingInfo.IsRowVersion,
+ mappingInfo.IsFixedLength,
+ mappingInfo.Precision,
+ mappingInfo.Scale);
+
+ if (FindMapping(stringMappingInfo) is not SqlServerStringTypeMapping stringTypeMapping)
+ {
+ return null;
+ }
+
+ stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping
+ .Clone(new CollectionToJsonStringConverter(mappingInfo.ClrType, elementTypeMapping));
+
+ // OpenJson was introduced in SQL Server 2016 (compatibility level 130). If the user configures an older compatibility level,
+ // we allow mapping the column, but don't set the element type mapping on the mapping, so that it isn't queryable.
+ // This causes us to go into the old translation path for Contains over parameter via IN with constants.
+ if (_supportsOpenJson)
+ {
+ stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping);
+ }
+
+ return stringTypeMapping;
+ }
+
private static readonly List NameBasesUsingPrecision = new()
{
"decimal",
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
index 140244153c2..28434f3f3bf 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
@@ -1,8 +1,10 @@
// 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;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Sqlite.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
@@ -14,6 +16,9 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
///
public class SqliteQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
///
/// 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
@@ -26,6 +31,8 @@ public SqliteQueryableMethodTranslatingExpressionVisitor(
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
+ _typeMappingSource = relationalDependencies.TypeMappingSource;
+ _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory;
}
///
@@ -38,6 +45,8 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor(
SqliteQueryableMethodTranslatingExpressionVisitor parentVisitor)
: base(parentVisitor)
{
+ _typeMappingSource = parentVisitor._typeMappingSource;
+ _sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
}
///
@@ -111,6 +120,104 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return translation;
}
+ ///
+ /// 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 ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate)
+ {
+ // Simplify x.Array.Count() => json_array_length(x.Array) instead of SELECT COUNT(*) FROM json_each(x.Array)
+ if (predicate is null && source.QueryExpression is SelectExpression
+ {
+ // TODO: Switch to JsonEachExpression
+ Tables: [TableValuedFunctionExpression { Name: "json_each", Arguments: [var array] }],
+ GroupBy: [],
+ Having: null,
+ IsDistinct: false,
+ Limit: null,
+ Offset: null
+ })
+ {
+ var translation = _sqlExpressionFactory.Function(
+ "json_array_length",
+ new[] { array },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true },
+ typeof(int));
+
+ return source.Update(_sqlExpressionFactory.Select(translation), source.ShaperExpression);
+ }
+
+ return base.TranslateCount(source, predicate);
+ }
+
+ ///
+ /// 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 ShapedQueryExpression? TranslatePrimitiveCollection(SqlExpression sqlExpression)
+ {
+ var elementClrType = sqlExpression.Type.GetSequenceType();
+ RelationalTypeMapping? elementTypeMapping = null;
+
+ switch (sqlExpression)
+ {
+ case ColumnExpression
+ {
+ TypeMapping: SqliteStringTypeMapping
+ {
+ Converter: CollectionToJsonStringConverter,
+ ElementTypeMapping: RelationalTypeMapping e
+ }
+ } column:
+ elementTypeMapping = e;
+ sqlExpression = column;
+ break;
+
+ case SqlParameterExpression parameterExpression:
+ // TODO: Hack until I finish proper type mapping inference on the parameter. We should pass null here as the parameter's
+ // type mapping, to allow it to properly get inferred later.
+ // After that, simplify this whole block.
+ sqlExpression = parameterExpression.ApplyTypeMapping(
+ _typeMappingSource.FindMapping(parameterExpression.Type));
+ break;
+
+ default:
+ return null;
+ }
+
+ // TODO: Decide what to do about type inference here. Not sure casting the output of json_each is actually needed.
+ // TODO: But there's also the SelectExpression projection.
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression
+ var jsonEachExpression = new TableValuedFunctionExpression("j", "json_each", schema: null, builtIn: true, new[] { sqlExpression });
+
+ // TODO: Probably move this up to relational...
+ if (elementTypeMapping is null)
+ {
+ RegisterUntypedColumnExpression(jsonEachExpression);
+ }
+
+ var selectExpression = new SelectExpression(elementClrType, elementTypeMapping, jsonEachExpression, "value");
+
+ Expression shaperExpression = new ProjectionBindingExpression(
+ selectExpression, new ProjectionMember(), elementClrType.MakeNullable());
+
+ if (elementClrType != shaperExpression.Type)
+ {
+ Check.DebugAssert(
+ elementClrType.MakeNullable() == shaperExpression.Type,
+ "expression.Type must be nullable of targetType");
+
+ shaperExpression = Expression.Convert(shaperExpression, elementClrType);
+ }
+
+ return new ShapedQueryExpression(selectExpression, shaperExpression);
+ }
+
private static Type GetProviderType(SqlExpression expression)
=> expression.TypeMapping?.Converter?.ProviderClrType
?? expression.TypeMapping?.ClrType
diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs
index 434c6766ad0..590417f511f 100644
--- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs
+++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs
@@ -47,6 +47,16 @@ protected SqliteStringTypeMapping(RelationalTypeMappingParameters parameters)
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new SqliteStringTypeMapping(parameters);
+ ///
+ /// 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 RelationalTypeMapping CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping)
+ => new SqliteStringTypeMapping(
+ Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)));
+
///
/// 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.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs
index 08ec4f9e887..8efc5b0eb40 100644
--- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs
+++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
+using Microsoft.Data.Sqlite;
namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
@@ -94,6 +95,8 @@ private static readonly HashSet SpatialiteTypes
{ TextTypeName, Text }
};
+ private readonly bool _areJsonFunctionsSupported;
+
///
/// 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
@@ -105,6 +108,9 @@ public SqliteTypeMappingSource(
RelationalTypeMappingSourceDependencies relationalDependencies)
: base(dependencies, relationalDependencies)
{
+ // Support for JSON functions was added in Sqlite 3.38.0 (2022-02-22, see https://www.sqlite.org/json1.html).
+ // This determines whether we have json_each, which is needed to query into JSON columns.
+ _areJsonFunctionsSupported = new Version(new SqliteConnection().ServerVersion) >= new Version(3, 38);
}
///
@@ -124,7 +130,9 @@ public static bool IsSpatialiteType(string columnType)
///
protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
{
- var mapping = base.FindMapping(mappingInfo) ?? FindRawMapping(mappingInfo);
+ var mapping = base.FindMapping(mappingInfo)
+ ?? FindRawMapping(mappingInfo)
+ ?? FindCollectionMapping(mappingInfo);
return mapping != null
&& mappingInfo.StoreTypeName != null
@@ -169,6 +177,55 @@ public static bool IsSpatialiteType(string columnType)
return null;
}
+ private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo)
+ {
+ // TODO: We currently support mapping any IList<>, do we want to support HashSet? Any collection? Compare to our policy for
+ // TODO: regular navigations, but remember that here we have order.
+ if (mappingInfo is { StoreTypeName: TextTypeName or null }
+ && mappingInfo.ClrType?.TryGetElementType(typeof(IList<>)) is { } elementClrType
+ && FindMapping(elementClrType) is { } elementTypeMapping)
+ {
+ var stringMappingInfo = new RelationalTypeMappingInfo(
+ typeof(string),
+ mappingInfo.StoreTypeName,
+ mappingInfo.StoreTypeNameBase,
+ mappingInfo.IsKeyOrIndex,
+ mappingInfo.IsUnicode,
+ mappingInfo.Size,
+ mappingInfo.IsRowVersion,
+ mappingInfo.IsFixedLength,
+ mappingInfo.Precision,
+ mappingInfo.Scale);
+
+ if (FindMapping(stringMappingInfo) is not SqliteStringTypeMapping stringTypeMapping)
+ {
+ return null;
+ }
+
+ // Specifically exclude collections over Geometry, since there's a dedicated GeometryCollection type for that (see #30630)
+ if (elementClrType.Namespace == "NetTopologySuite.Geometries")
+ {
+ return null;
+ }
+
+ stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping
+ .Clone(new CollectionToJsonStringConverter(mappingInfo.ClrType, elementTypeMapping));
+
+ // json_each was introduced in SQLite 3.38.0; on older SQLite version we allow mapping the column, but don't set the element
+ // type mapping on the mapping, so that it isn't queryable. This causes us to go into the old translation path for Contains
+ // over parameter via IN with constants.
+ if (_areJsonFunctionsSupported)
+ {
+ stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping
+ .CloneWithElementTypeMapping(elementTypeMapping);
+ }
+
+ return stringTypeMapping;
+ }
+
+ return null;
+ }
+
private readonly Func[] _typeRules =
{
name => Contains(name, "INT")
diff --git a/src/EFCore/Query/ConstantQueryRootExpression.cs b/src/EFCore/Query/ConstantQueryRootExpression.cs
new file mode 100644
index 00000000000..c432cdc3804
--- /dev/null
+++ b/src/EFCore/Query/ConstantQueryRootExpression.cs
@@ -0,0 +1,79 @@
+// 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;
+
+///
+///
+/// An expression that represents a constant query root within the query.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class ConstantQueryRootExpression : QueryRootExpression
+{
+ ///
+ /// A constant value containing the values that this query root represents.
+ ///
+ public ConstantExpression ConstantExpression { get; }
+
+ // TODO: Currently must be an array, relax to allow arbitrary enumerables?
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The query provider associated with this query root.
+ /// A constant value containing the values that this query root represents.
+ public ConstantQueryRootExpression(IAsyncQueryProvider asyncQueryProvider, ConstantExpression constantExpression)
+ : base(
+ asyncQueryProvider,
+ constantExpression.Type.TryGetSequenceType() ?? throw new ArgumentException("Must be an enumerable")) // TODO
+ {
+ ConstantExpression = constantExpression;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A constant value containing the values that this query root represents.
+ public ConstantQueryRootExpression(ConstantExpression constantExpression)
+ : base(
+ constantExpression.Type.TryGetSequenceType() ?? throw new ArgumentException("Must be an enumerable")) // TODO
+ {
+ ConstantExpression = constantExpression;
+ }
+
+ ///
+ public override Expression DetachQueryProvider()
+ => new ConstantQueryRootExpression(ConstantExpression);
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var visited = visitor.Visit(ConstantExpression);
+
+ return visited == ConstantExpression
+ ? this
+ : new ConstantQueryRootExpression((ConstantExpression)visited);
+ }
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("[");
+
+ var array = (Array)ConstantExpression.Value!;
+ for (var i = 0; i < array.Length; i++)
+ {
+ if (i > 0)
+ {
+ expressionPrinter.Append(",");
+ }
+
+ expressionPrinter.Append(array.GetValue(i)?.ToString() ?? "");
+ }
+
+ expressionPrinter.Append("]");
+ }
+}
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs
index a09f19db5d3..cc60ba7b4e6 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs
@@ -104,22 +104,34 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
}
- var navigation = memberIdentity.MemberInfo != null
+ var navigation = memberIdentity.MemberInfo is not null
? entityType.FindNavigation(memberIdentity.MemberInfo)
- : entityType.FindNavigation(memberIdentity.Name!);
- if (navigation != null)
+ : memberIdentity.Name is not null
+ ? entityType.FindNavigation(memberIdentity.Name)
+ : null;
+ if (navigation is not null)
{
- return ExpandNavigation(root, entityReference, navigation, convertedType != null);
+ return ExpandNavigation(root, entityReference, navigation, convertedType is not null);
}
- var skipNavigation = memberIdentity.MemberInfo != null
+ var skipNavigation = memberIdentity.MemberInfo is not null
? entityType.FindSkipNavigation(memberIdentity.MemberInfo)
: memberIdentity.Name is not null
? entityType.FindSkipNavigation(memberIdentity.Name)
: null;
- if (skipNavigation != null)
+ if (skipNavigation is not null)
+ {
+ return ExpandSkipNavigation(root, entityReference, skipNavigation, convertedType is not null);
+ }
+
+ var property = memberIdentity.MemberInfo != null
+ ? entityType.FindProperty(memberIdentity.MemberInfo)
+ : memberIdentity.Name is not null
+ ? entityType.FindProperty(memberIdentity.Name)
+ : null;
+ if (property?.GetTypeMapping().ElementTypeMapping != null)
{
- return ExpandSkipNavigation(root, entityReference, skipNavigation, convertedType != null);
+ return new QueryablePropertyReference(root, property);
}
}
@@ -1015,6 +1027,9 @@ private sealed class ReducingExpressionVisitor : ExpressionVisitor
case OwnedNavigationReference ownedNavigationReference:
return Visit(ownedNavigationReference.Parent).CreateEFPropertyExpression(ownedNavigationReference.Navigation);
+ case QueryablePropertyReference queryablePropertyReference:
+ return Visit(queryablePropertyReference.Parent).CreateEFPropertyExpression(queryablePropertyReference.Property);
+
case IncludeExpression includeExpression:
var entityExpression = Visit(includeExpression.EntityExpression);
var navigationExpression = ReplacingExpressionVisitor.Replace(
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
index 69d8229eb31..7f9abec9c87 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
@@ -501,4 +501,46 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
}
}
}
+
+ ///
+ /// Queryable properties are not expanded (similar to .
+ ///
+ private sealed class QueryablePropertyReference : Expression, IPrintableExpression
+ {
+ public QueryablePropertyReference(Expression parent, IProperty property /*, EntityReference entityReference */)
+ {
+ Parent = parent;
+ Property = property;
+ // EntityReference = entityReference;
+ }
+
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ Parent = visitor.Visit(Parent);
+
+ return this;
+ }
+
+ public Expression Parent { get; private set; }
+ public new IProperty Property { get; }
+ // public EntityReference EntityReference { get; }
+
+ public override Type Type
+ => Property.ClrType;
+
+ public override ExpressionType NodeType
+ => ExpressionType.Extension;
+
+ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.AppendLine(nameof(OwnedNavigationReference));
+ using (expressionPrinter.Indent())
+ {
+ expressionPrinter.Append("Parent: ");
+ expressionPrinter.Visit(Parent);
+ expressionPrinter.AppendLine();
+ expressionPrinter.Append("Property: " + Property.Name + " (QUERYABLE)");
+ }
+ }
+ }
}
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
index f6fedc5ac46..51496705bd9 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
@@ -760,8 +760,7 @@ when QueryableMethods.IsSumWithSelector(method):
if (genericMethod == QueryableMethods.AsQueryable)
{
- if (firstArgument is NavigationTreeExpression navigationTreeExpression
- && navigationTreeExpression.Type.IsGenericType
+ if (firstArgument is NavigationTreeExpression { Type.IsGenericType: true } navigationTreeExpression
&& navigationTreeExpression.Type.GetGenericTypeDefinition() == typeof(IGrouping<,>))
{
// This is groupingElement.AsQueryable so we preserve it
@@ -770,6 +769,7 @@ when QueryableMethods.IsSumWithSelector(method):
navigationTreeExpression);
}
+ // HACK
return UnwrapCollectionMaterialization(firstArgument);
}
@@ -782,7 +782,8 @@ when QueryableMethods.IsSumWithSelector(method):
return ConvertToEnumerable(method, visitedArguments);
}
- throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print()));
+ return base.VisitMethodCall(methodCallExpression);
+ // throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print()));
}
// Remove MaterializeCollectionNavigationExpression when applying ToList/ToArray
@@ -1954,17 +1955,6 @@ private NavigationExpansionExpression CreateNavigationExpansionExpression(
return new NavigationExpansionExpression(sourceExpression, currentTree, currentTree, parameterName);
}
- private NavigationExpansionExpression CreateNavigationExpansionExpression(
- Expression sourceExpression,
- OwnedNavigationReference ownedNavigationReference)
- {
- var parameterName = GetParameterName("o");
- var entityReference = ownedNavigationReference.EntityReference;
- var currentTree = new NavigationTreeExpression(entityReference);
-
- return new NavigationExpansionExpression(sourceExpression, currentTree, currentTree, parameterName);
- }
-
private Expression ExpandNavigationsForSource(NavigationExpansionExpression source, Expression expression)
{
expression = _removeRedundantNavigationComparisonExpressionVisitor.Visit(expression);
@@ -2033,8 +2023,7 @@ private LambdaExpression GenerateLambda(Expression body, ParameterExpression cur
private Expression UnwrapCollectionMaterialization(Expression expression)
{
- while (expression is MethodCallExpression innerMethodCall
- && innerMethodCall.Method.IsGenericMethod
+ while (expression is MethodCallExpression { Method.IsGenericMethod: true } innerMethodCall
&& innerMethodCall.Method.GetGenericMethodDefinition() is MethodInfo innerMethod
&& (innerMethod == EnumerableMethods.AsEnumerable
|| innerMethod == EnumerableMethods.ToList
@@ -2048,14 +2037,38 @@ private Expression UnwrapCollectionMaterialization(Expression expression)
expression = materializeCollectionNavigationExpression.Subquery;
}
- return expression is OwnedNavigationReference ownedNavigationReference
- && ownedNavigationReference.Navigation.IsCollection
- ? CreateNavigationExpansionExpression(
+ switch (expression)
+ {
+ case OwnedNavigationReference { Navigation.IsCollection: true } ownedNavigationReference:
+ {
+ var currentTree = new NavigationTreeExpression(ownedNavigationReference.EntityReference);
+
+ return new NavigationExpansionExpression(
Expression.Call(
QueryableMethods.AsQueryable.MakeGenericMethod(ownedNavigationReference.Type.GetSequenceType()),
ownedNavigationReference),
- ownedNavigationReference)
- : expression;
+ currentTree,
+ currentTree,
+ GetParameterName("o"));
+ }
+
+ case QueryablePropertyReference queryablePropertyReference:
+ {
+ var currentTree = new NavigationTreeExpression(Expression.Default(queryablePropertyReference.Type.GetSequenceType()));
+
+ return new NavigationExpansionExpression(
+ Expression.Call(
+ QueryableMethods.AsQueryable.MakeGenericMethod(queryablePropertyReference.Type.GetSequenceType()),
+ queryablePropertyReference),
+ currentTree,
+ currentTree,
+ GetParameterName("p"));
+
+ }
+
+ default:
+ return expression;
+ }
}
private string GetParameterName(string prefix)
@@ -2242,26 +2255,19 @@ private static Expression SnapshotExpression(Expression selector)
}
private static EntityReference? UnwrapEntityReference(Expression? expression)
- {
- switch (expression)
+ => expression switch
{
- case EntityReference entityReference:
- return entityReference;
-
- case NavigationTreeExpression navigationTreeExpression:
- return UnwrapEntityReference(navigationTreeExpression.Value);
-
- case NavigationExpansionExpression navigationExpansionExpression
- when navigationExpansionExpression.CardinalityReducingGenericMethodInfo != null:
- return UnwrapEntityReference(navigationExpansionExpression.PendingSelector);
-
- case OwnedNavigationReference ownedNavigationReference:
- return ownedNavigationReference.EntityReference;
-
- default:
- return null;
- }
- }
+ EntityReference entityReference
+ => entityReference,
+ NavigationTreeExpression navigationTreeExpression
+ => UnwrapEntityReference(navigationTreeExpression.Value),
+ NavigationExpansionExpression navigationExpansionExpression
+ when navigationExpansionExpression.CardinalityReducingGenericMethodInfo != null
+ => UnwrapEntityReference(navigationExpansionExpression.PendingSelector),
+ OwnedNavigationReference ownedNavigationReference
+ => ownedNavigationReference.EntityReference,
+ _ => null
+ };
private sealed class Parameters : IParameterValues
{
diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
index b8ae88c3639..cf44649dea8 100644
--- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
@@ -66,8 +66,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
visitedExpression = TryConvertEnumerableToQueryable(methodCallExpression);
}
- if (method.DeclaringType != null
- && method.DeclaringType.IsGenericType
+ if (method.DeclaringType is { IsGenericType: true }
&& (method.DeclaringType.GetGenericTypeDefinition() == typeof(ICollection<>)
|| method.DeclaringType.GetGenericTypeDefinition() == typeof(List<>))
&& method.Name == nameof(List.Contains))
@@ -251,20 +250,27 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa
return base.VisitMethodCall(methodCallExpression);
}
+ // HACK
if (methodCallExpression.Arguments.Count > 0
- && ClientSource(methodCallExpression.Arguments[0]))
+ && methodCallExpression.Arguments[0] is MemberInitExpression or NewExpression)
{
- // this is methodCall over closure variable or constant
return base.VisitMethodCall(methodCallExpression);
}
+ // HACK
+ // if (methodCallExpression.Arguments.Count > 0
+ // && ClientSource(methodCallExpression.Arguments[0]))
+ // {
+ // // this is methodCall over closure variable or constant
+ // return base.VisitMethodCall(methodCallExpression);
+ // }
+
var arguments = VisitAndConvert(methodCallExpression.Arguments, nameof(VisitMethodCall)).ToArray();
var enumerableMethod = methodCallExpression.Method;
var enumerableParameters = enumerableMethod.GetParameters();
var genericTypeArguments = Array.Empty();
- if (enumerableMethod.Name == nameof(Enumerable.Min)
- || enumerableMethod.Name == nameof(Enumerable.Max))
+ if (enumerableMethod.Name is nameof(Enumerable.Min) or nameof(Enumerable.Max))
{
genericTypeArguments = new Type[methodCallExpression.Arguments.Count];
@@ -332,8 +338,7 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa
// If innerArgument has ToList applied to it then unwrap it.
// Also preserve generic argument of ToList is applied to different type
if (arguments[i].Type.TryGetElementType(typeof(List<>)) != null
- && arguments[i] is MethodCallExpression toListMethodCallExpression
- && toListMethodCallExpression.Method.IsGenericMethod
+ && arguments[i] is MethodCallExpression { Method.IsGenericMethod: true } toListMethodCallExpression
&& toListMethodCallExpression.Method.GetGenericMethodDefinition() == EnumerableMethods.ToList)
{
genericType = toListMethodCallExpression.Method.GetGenericArguments()[0];
@@ -386,9 +391,9 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa
private Expression TryConvertListContainsToQueryableContains(MethodCallExpression methodCallExpression)
{
- if (ClientSource(methodCallExpression.Object))
+ // if (ClientSource(methodCallExpression.Object))
+ if (methodCallExpression.Object is ConstantExpression or MemberInitExpression or NewExpression)
{
- // this is methodCall over closure variable or constant
return base.VisitMethodCall(methodCallExpression);
}
@@ -402,12 +407,10 @@ private Expression TryConvertListContainsToQueryableContains(MethodCallExpressio
methodCallExpression.Arguments[0]);
}
- private static bool ClientSource(Expression? expression)
- => expression is ConstantExpression
- || expression is MemberInitExpression
- || expression is NewExpression
- || expression is ParameterExpression parameter
- && parameter.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true;
+ // private static bool ClientSource(Expression? expression)
+ // => expression is ConstantExpression or MemberInitExpression or NewExpression
+ // || expression is ParameterExpression parameter
+ // && parameter.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true;
private static bool CanConvertEnumerableToQueryable(Type enumerableType, Type queryableType)
{
@@ -438,8 +441,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression
{
// SelectMany
var selectManySource = methodCallExpression.Arguments[0];
- if (selectManySource is MethodCallExpression groupJoinMethod
- && groupJoinMethod.Method.IsGenericMethod
+ if (selectManySource is MethodCallExpression { Method.IsGenericMethod: true } groupJoinMethod
&& groupJoinMethod.Method.GetGenericMethodDefinition() == QueryableMethods.GroupJoin)
{
// GroupJoin
@@ -455,8 +457,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression
var collectionSelectorBody = selectManyCollectionSelector.Body;
var defaultIfEmpty = false;
- if (collectionSelectorBody is MethodCallExpression collectionEndingMethod
- && collectionEndingMethod.Method.IsGenericMethod
+ if (collectionSelectorBody is MethodCallExpression { Method.IsGenericMethod: true } collectionEndingMethod
&& collectionEndingMethod.Method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument)
{
defaultIfEmpty = true;
@@ -478,8 +479,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression
ReplacingExpressionVisitor.Replace(
groupJoinResultSelector.Parameters[1], inner, collectionSelectorBody));
- if (inner is MethodCallExpression innerMethodCall
- && innerMethodCall.Method.IsGenericMethod
+ if (inner is MethodCallExpression { Method.IsGenericMethod: true } innerMethodCall
&& innerMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable
&& innerMethodCall.Type == innerMethodCall.Arguments[0].Type)
{
@@ -542,8 +542,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression
{
// SelectMany
var selectManySource = methodCallExpression.Arguments[0];
- if (selectManySource is MethodCallExpression groupJoinMethod
- && groupJoinMethod.Method.IsGenericMethod
+ if (selectManySource is MethodCallExpression { Method.IsGenericMethod: true } groupJoinMethod
&& groupJoinMethod.Method.GetGenericMethodDefinition() == QueryableMethods.GroupJoin)
{
// GroupJoin
@@ -558,8 +557,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression
var groupJoinResultSelectorBody = groupJoinResultSelector.Body;
var defaultIfEmpty = false;
- if (groupJoinResultSelectorBody is MethodCallExpression collectionEndingMethod
- && collectionEndingMethod.Method.IsGenericMethod
+ if (groupJoinResultSelectorBody is MethodCallExpression { Method.IsGenericMethod: true } collectionEndingMethod
&& collectionEndingMethod.Method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument)
{
defaultIfEmpty = true;
diff --git a/src/EFCore/Query/ParameterQueryRootExpression.cs b/src/EFCore/Query/ParameterQueryRootExpression.cs
new file mode 100644
index 00000000000..e61506d2bb2
--- /dev/null
+++ b/src/EFCore/Query/ParameterQueryRootExpression.cs
@@ -0,0 +1,63 @@
+// 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;
+
+///
+///
+/// An expression that represents a parameter query root within the query.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class ParameterQueryRootExpression : QueryRootExpression
+{
+ ///
+ /// The parameter expression representing the values for this query root.
+ ///
+ public ParameterExpression ParameterExpression { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The query provider associated with this query root.
+ /// The values that this query root represents.
+ /// The parameter expression representing the values for this query root.
+ public ParameterQueryRootExpression(
+ IAsyncQueryProvider asyncQueryProvider, Type elementType, ParameterExpression parameterExpression)
+ : base(asyncQueryProvider, elementType)
+ {
+ ParameterExpression = parameterExpression;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The values that this query root represents.
+ /// The parameter expression representing the values for this query root.
+ public ParameterQueryRootExpression(Type elementType, ParameterExpression parameterExpression)
+ : base(elementType)
+ {
+ ParameterExpression = parameterExpression;
+ }
+
+ ///
+ public override Expression DetachQueryProvider()
+ => new ParameterQueryRootExpression(ElementType, ParameterExpression);
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var parameterExpression = (ParameterExpression)visitor.Visit(ParameterExpression);
+
+ return parameterExpression == ParameterExpression
+ ? this
+ : new ParameterQueryRootExpression(ElementType, parameterExpression);
+ }
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ => expressionPrinter.Visit(ParameterExpression);
+}
diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs
index c83a7c74910..e51a1ab42f4 100644
--- a/src/EFCore/Query/QueryCompilationContext.cs
+++ b/src/EFCore/Query/QueryCompilationContext.cs
@@ -163,7 +163,7 @@ public virtual Func CreateQueryExecutor(Expressi
query = _queryTranslationPreprocessorFactory.Create(this).Process(query);
// Convert EntityQueryable to ShapedQueryExpression
- query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Visit(query);
+ query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(query);
query = _queryTranslationPostprocessorFactory.Create(this).Process(query);
// Inject actual entity materializer
diff --git a/src/EFCore/Query/QueryRootProcessor.cs b/src/EFCore/Query/QueryRootProcessor.cs
new file mode 100644
index 00000000000..ff6a282296c
--- /dev/null
+++ b/src/EFCore/Query/QueryRootProcessor.cs
@@ -0,0 +1,99 @@
+// 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;
+
+///
+/// A visitor which adds additional query root nodes during preprocessing.
+///
+public class QueryRootProcessor : ExpressionVisitor
+{
+ ///
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ // We cannot convert all arrays/enumerables everywhere to query roots, since query roots have an expression type of IQueryable,
+ // and that would be incompatible e.g. with an array parameter in some regular function.
+ // So for now, we only convert direct arguments where the parameter is IEnumerable/IQueryable; this is very conservative and may
+ // miss some cases, but it's safer and less likely to introduce bugs.
+ // Note that we unwrap constant and parameter query roots in SqlTranslatingEV to their wrapped constant/parameter in case they make
+ // it there; they're only translated to SQL query root expressions in QueryableMethodTranslatingEV.
+ var method = methodCallExpression.Method;
+ var parameters = method.GetParameters();
+
+ // Note that we don't need to look at methodCallExpression.Object, since IQueryable<> doesn't declare any methods.
+ // All methods over queryable are extensions.
+ Expression[]? newArguments = null;
+
+ for (var i = 0; i < methodCallExpression.Arguments.Count; i++)
+ {
+ var argument = methodCallExpression.Arguments[i];
+ var parameterType = parameters[i].ParameterType;
+
+ Expression? visitedArgument = null;
+
+ if (parameterType.IsGenericType
+ && (parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
+ || parameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)))
+ {
+ visitedArgument = argument switch
+ {
+ ConstantExpression constantArgument
+ => VisitQueryableConstant(constantArgument),
+
+ ParameterExpression parameterExpression
+ when parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)
+ == true
+ => VisitQueryableParameter(parameterExpression),
+
+ _ => null
+ };
+ }
+
+ visitedArgument ??= Visit(argument);
+
+ if (visitedArgument != argument)
+ {
+ if (newArguments is null)
+ {
+ newArguments = new Expression[methodCallExpression.Arguments.Count];
+
+ for (var j = 0; j < i; j++)
+ {
+ newArguments[j] = methodCallExpression.Arguments[j];
+ }
+ }
+ }
+
+ if (newArguments is not null)
+ {
+ newArguments[i] = visitedArgument;
+ }
+ }
+
+ return newArguments is null
+ ? methodCallExpression
+ : methodCallExpression.Update(methodCallExpression.Object, newArguments);
+ }
+
+ ///
+ /// A provider hook for converting a to a , which
+ /// would be translated later e.g. by converting its contents to a JSON string.
+ ///
+ ///
+ /// Returns the given argument by default, since parameters query roots are a provider-specific capability.
+ ///
+ /// The parameter expression to attempt to convert to a query root.
+ protected virtual Expression VisitQueryableParameter(ParameterExpression parameterExpression)
+ => parameterExpression;
+
+ ///
+ /// A provider hook for converting a to a .
+ ///
+ ///
+ /// Returns the given argument by default, since constant query roots are a provider-specific capability.
+ ///
+ /// The constant expression to attempt to convert to a query root.
+ protected virtual Expression VisitQueryableConstant(ConstantExpression constantExpression)
+ => constantExpression;
+}
+
diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs
index 1f2a0cc307b..6ff2be35925 100644
--- a/src/EFCore/Query/QueryTranslationPreprocessor.cs
+++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs
@@ -52,6 +52,7 @@ public virtual Expression Process(Expression query)
{
query = new InvocationExpressionRemovingExpressionVisitor().Visit(query);
query = NormalizeQueryableMethod(query);
+ query = ProcessQueryRoots(query);
query = new NullCheckRemovingExpressionVisitor().Visit(query);
query = new SubqueryMemberPushdownExpressionVisitor(QueryCompilationContext.Model).Visit(query);
query = new NavigationExpandingExpressionVisitor(
@@ -77,6 +78,17 @@ public virtual Expression Process(Expression query)
/// The query expression to normalize.
/// A query expression after normalization has been done.
public virtual Expression NormalizeQueryableMethod(Expression expression)
- => new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext)
- .Normalize(expression);
+ {
+ expression = new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext).Normalize(expression);
+
+ return expression;
+ }
+
+ ///
+ /// Adds additional query root nodes to the query.
+ ///
+ /// The query expression to process.
+ /// A query expression after query roots have been added.
+ public virtual Expression ProcessQueryRoots(Expression expression)
+ => expression;
}
diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
index 4b90c04448f..0868932c14b 100644
--- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
@@ -50,6 +50,14 @@ protected QueryableMethodTranslatingExpressionVisitor(
///
public virtual string? TranslationErrorDetails { get; private set; }
+ ///
+ /// Translates an expression to an equivalent SQL representation.
+ ///
+ /// An expression to translate.
+ /// A SQL translation of the given expression.
+ public virtual Expression Translate(Expression expression)
+ => Visit(expression);
+
///
/// Adds detailed information about errors encountered during translation.
///
@@ -85,7 +93,10 @@ protected override Expression VisitExtension(Expression extensionExpression)
}
throw new InvalidOperationException(
- CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName()));
+ CoreStrings.TranslationFailedWithDetails(
+ queryRootExpression,
+ TranslationErrorDetails
+ ?? CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName())));
}
return base.VisitExtension(extensionExpression);
@@ -508,7 +519,7 @@ protected virtual Expression MarkShaperNullable(Expression shaperExpression)
public virtual ShapedQueryExpression? TranslateSubquery(Expression expression)
{
var subqueryVisitor = CreateSubqueryVisitor();
- var translation = subqueryVisitor.Visit(expression) as ShapedQueryExpression;
+ var translation = subqueryVisitor.Translate(expression) as ShapedQueryExpression;
if (translation == null && subqueryVisitor.TranslationErrorDetails != null)
{
AddTranslationErrorDetails(subqueryVisitor.TranslationErrorDetails);
diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs
index bc39feacff8..04188d2eb11 100644
--- a/src/EFCore/Storage/CoreTypeMapping.cs
+++ b/src/EFCore/Storage/CoreTypeMapping.cs
@@ -35,13 +35,17 @@ protected readonly record struct CoreTypeMappingParameters
/// Supports custom comparisons between keys--e.g. PK to FK comparison.
/// Supports custom comparisons between converted provider values.
/// An optional factory for creating a specific .
+ ///
+ /// If this type mapping represents a primitive collection, this holds the element's type mapping.
+ ///
public CoreTypeMappingParameters(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type clrType,
ValueConverter? converter = null,
ValueComparer? comparer = null,
ValueComparer? keyComparer = null,
ValueComparer? providerValueComparer = null,
- Func? valueGeneratorFactory = null)
+ Func? valueGeneratorFactory = null,
+ CoreTypeMapping? elementTypeMapping = null)
{
ClrType = clrType;
Converter = converter;
@@ -49,6 +53,7 @@ public CoreTypeMappingParameters(
KeyComparer = keyComparer;
ProviderValueComparer = providerValueComparer;
ValueGeneratorFactory = valueGeneratorFactory;
+ ElementTypeMapping = elementTypeMapping;
}
///
@@ -83,6 +88,11 @@ public CoreTypeMappingParameters(
///
public Func? ValueGeneratorFactory { get; }
+ ///
+ /// If this type mapping represents a primitive collection, this holds the element's type mapping.
+ ///
+ public CoreTypeMapping? ElementTypeMapping { get; }
+
///
/// Creates a new parameter object with the given
/// converter composed with any existing converter and set on the new parameter object.
@@ -96,7 +106,24 @@ public CoreTypeMappingParameters WithComposedConverter(ValueConverter? converter
Comparer,
KeyComparer,
ProviderValueComparer,
- ValueGeneratorFactory);
+ ValueGeneratorFactory,
+ ElementTypeMapping);
+
+ ///
+ /// Creates a new parameter object with the given
+ /// element type mapping.
+ ///
+ /// The element type mapping.
+ /// The new parameter object.
+ public CoreTypeMappingParameters WithElementTypeMapping(CoreTypeMapping elementTypeMapping)
+ => new(
+ ClrType,
+ Converter,
+ Comparer,
+ KeyComparer,
+ ProviderValueComparer,
+ ValueGeneratorFactory,
+ elementTypeMapping);
}
private ValueComparer? _comparer;
@@ -224,4 +251,10 @@ public virtual ValueComparer ProviderValueComparer
/// An expression tree that can be used to generate code for the literal value.
public virtual Expression GenerateCodeLiteral(object value)
=> throw new NotSupportedException(CoreStrings.LiteralGenerationNotSupported(ClrType.ShortDisplayName()));
+
+ ///
+ /// If this type mapping represents a primitive collection, this holds the element's type mapping.
+ ///
+ public virtual CoreTypeMapping? ElementTypeMapping
+ => Parameters.ElementTypeMapping;
}
diff --git a/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs b/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs
new file mode 100644
index 00000000000..3a3342ccec6
--- /dev/null
+++ b/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+
+namespace Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+///
+/// A value converter that converts a .NET primitive collection into a JSON string.
+///
+// TODO: This currently just calls JsonSerialize.Serialize/Deserialize. It should go through the element type mapping's APIs for
+// serializing/deserializing JSON instead, when those APIs are introduced.
+// TODO: Nulls? Mapping hints? Customizable JsonSerializerOptions?
+public class CollectionToJsonStringConverter : ValueConverter
+{
+ private readonly CoreTypeMapping _elementTypeMapping;
+
+ ///
+ /// Creates a new instance of this converter.
+ ///
+ ///
+ /// See EF Core value converters for more information and examples.
+ ///
+ public CollectionToJsonStringConverter(Type modelClrType, CoreTypeMapping elementTypeMapping)
+ : base(
+ (Expression>)(x => JsonSerializer.Serialize(x, (JsonSerializerOptions?)null)),
+ (Expression>)(s => JsonSerializer.Deserialize(s, modelClrType, (JsonSerializerOptions?)null)!)) // TODO: Nullability
+ {
+ ModelClrType = modelClrType;
+ _elementTypeMapping = elementTypeMapping;
+
+ // TODO: Do we support value conversion on the *element* type mapping? That would mean another special API to configure that in
+ // metadata, and we'd need to apply it here for every element. Otherwise, only array-level converters would be supported, and we
+ // only see the already-converted values here.
+ // TODO: Full sanitization/nullability
+ ConvertToProvider = x => JsonSerializer.Serialize(x);
+ ConvertFromProvider = o
+ => o is string s
+ ? JsonSerializer.Deserialize(s, modelClrType)!
+ : throw new ArgumentException(); // TODO
+ }
+
+ ///
+ public override Func