diff --git a/All.sln.DotSettings b/All.sln.DotSettings
index 8229c429771..edd36d48c74 100644
--- a/All.sln.DotSettings
+++ b/All.sln.DotSettings
@@ -308,9 +308,12 @@ The .NET Foundation licenses this file to you under the MIT license.
TrueTrueTrue
+ TrueTrueTrue
+ TrueTrue
+ TrueTrueTrueTrue
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index fcc660d5a5f..0fe96122904 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -167,6 +167,14 @@ public static string ConflictingRowValuesSensitive(object? firstEntityType, obje
GetString("ConflictingRowValuesSensitive", nameof(firstEntityType), nameof(secondEntityType), nameof(keyValue), nameof(firstConflictingValue), nameof(secondConflictingValue), nameof(column)),
firstEntityType, secondEntityType, keyValue, firstConflictingValue, secondConflictingValue, column);
+ ///
+ /// Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'.
+ ///
+ public static string ConflictingTypeMappingsForPrimitiveCollection(object? storeType1, object? storeType2)
+ => string.Format(
+ GetString("ConflictingTypeMappingsForPrimitiveCollection", nameof(storeType1), nameof(storeType2)),
+ storeType1, storeType2);
+
///
/// A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
///
@@ -621,6 +629,12 @@ public static string EitherOfTwoValuesMustBeNull(object? param1, object? param2)
GetString("EitherOfTwoValuesMustBeNull", nameof(param1), nameof(param2)),
param1, param2);
+ ///
+ /// Empty collections are not supported as constant query roots.
+ ///
+ public static string EmptyCollectionNotSupportedAsInlineQueryRoot
+ => GetString("EmptyCollectionNotSupportedAsInlineQueryRoot");
+
///
/// 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").
///
@@ -1409,6 +1423,12 @@ public static string OptionalDependentWithDependentWithoutIdentifyingProperty(ob
GetString("OptionalDependentWithDependentWithoutIdentifyingProperty", nameof(entityType)),
entityType);
+ ///
+ /// Only constants are supported inside inline collection query roots.
+ ///
+ public static string OnlyConstantsSupportedInInlineCollectionQueryRoots
+ => GetString("OnlyConstantsSupportedInInlineCollectionQueryRoots");
+
///
/// Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[].
///
@@ -1966,7 +1986,7 @@ public static string UnsupportedOperatorForSqlExpression(object? nodeType, objec
nodeType, expressionType);
///
- /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'.
+ /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'.
///
public static string UnsupportedPropertyType(object? entity, object? property, object? clrType)
=> string.Format(
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 128be2b2177..938c9f6d455 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -175,6 +175,9 @@
Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.
+
+ Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'.
+
A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
@@ -346,6 +349,9 @@
Either {param1} or {param2} must be null.
+
+ Empty collections are not supported as inline 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").
@@ -950,6 +956,9 @@
Entity type '{entityType}' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.
+
+ Only constants are supported inside inline collection query roots.
+
Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[].
diff --git a/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs b/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs
index 69d98027268..ad58fbe7636 100644
--- a/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs
+++ b/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs
@@ -60,8 +60,10 @@ public EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
&& right != null)
{
if (left.Type == right.Type
- || (right.Type == typeof(object) && (right is SqlParameterExpression || right is SqlConstantExpression))
- || (left.Type == typeof(object) && (left is SqlParameterExpression || left is SqlConstantExpression)))
+ || (right.Type == typeof(object)
+ && right is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null })
+ || (left.Type == typeof(object)
+ && left is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null }))
{
return _sqlExpressionFactory.Equal(left, right);
}
diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index 94ce376d396..efac1cd435b 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -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.
///
@@ -371,16 +403,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)
@@ -607,19 +639,22 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
{
var invariantName = sqlParameterExpression.Name;
var parameterName = sqlParameterExpression.Name;
+ var typeMapping = sqlParameterExpression.TypeMapping!;
// Try to see if a parameter already exists - if so, just integrate the same placeholder into the SQL instead of sending the same
// data twice.
// Note that if the type mapping differs, we do send the same data twice (e.g. the same string may be sent once as Unicode, once as
// non-Unicode).
+ // TODO: Note that we perform Equals comparison on the value converter. We should be able to do reference comparison, but for
+ // that we need to ensure that there's only ever one type mapping instance (i.e. no type mappings are ever instantiated out of the
+ // type mapping source). See #30677.
var parameter = _relationalCommandBuilder.Parameters.FirstOrDefault(
p =>
p.InvariantName == parameterName
- && p is TypeMappedRelationalParameter typeMappedRelationalParameter
- && string.Equals(
- typeMappedRelationalParameter.RelationalTypeMapping.StoreType, sqlParameterExpression.TypeMapping!.StoreType,
- StringComparison.OrdinalIgnoreCase)
- && typeMappedRelationalParameter.RelationalTypeMapping.Converter == sqlParameterExpression.TypeMapping!.Converter);
+ && p is TypeMappedRelationalParameter { RelationalTypeMapping: var existingTypeMapping }
+ && string.Equals(existingTypeMapping.StoreType, typeMapping.StoreType, StringComparison.OrdinalIgnoreCase)
+ && (existingTypeMapping.Converter is null && typeMapping.Converter is null
+ || existingTypeMapping.Converter is not null && existingTypeMapping.Converter.Equals(typeMapping.Converter)));
if (parameter is null)
{
@@ -1132,6 +1167,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.
///
@@ -1311,6 +1368,65 @@ 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++)
+ {
+ 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..d2c218bb5c9
--- /dev/null
+++ b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs
@@ -0,0 +1,62 @@
+// 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 IModel _model;
+
+ ///
+ /// 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 RelationalQueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, queryCompilationContext)
+ {
+ _model = queryCompilationContext.Model;
+ }
+
+ ///
+ /// Indicates that a can be converted to a ;
+ /// this will later be translated to a SQL .
+ ///
+ protected override bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression)
+ => true;
+
+ ///
+ 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);
+ }
+
+ ///
+ 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..1d51f2f604b 100644
--- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
@@ -35,10 +35,13 @@ public RelationalQueryTranslationPreprocessor(
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);
+ expression = base.NormalizeQueryableMethod(expression);
return expression;
}
+
+ ///
+ protected override Expression ProcessQueryRoots(Expression expression)
+ => new RelationalQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext).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 9c6ee1a791a..536749f75ce 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -12,10 +11,14 @@ namespace Microsoft.EntityFrameworkCore.Query;
///
public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor
{
+ private const string SqlQuerySingleColumnAlias = "Value";
+ private const string ValuesOrderingColumnName = "_ord", ValuesValueColumnName = "Value";
+
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;
private readonly SharedTypeEntityExpandingExpressionVisitor _sharedTypeEntityExpandingExpressionVisitor;
private readonly RelationalProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor;
private readonly QueryCompilationContext _queryCompilationContext;
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly bool _subquery;
@@ -39,6 +42,7 @@ public RelationalQueryableMethodTranslatingExpressionVisitor(
_sharedTypeEntityExpandingExpressionVisitor =
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory);
_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
+ _typeMappingSource = relationalDependencies.TypeMappingSource;
_sqlExpressionFactory = sqlExpressionFactory;
_subquery = false;
}
@@ -63,10 +67,32 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor(
_sharedTypeEntityExpandingExpressionVisitor =
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory);
_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
+ _typeMappingSource = parentVisitor._typeMappingSource;
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
_subquery = true;
}
+ ///
+ public override Expression Translate(Expression expression)
+ {
+ var visited = Visit(expression);
+
+ if (!_subquery)
+ {
+ // We've finished translating the entire query.
+
+ // If any constant/parameter query roots exist in the query, their columns don't yet have a type mapping.
+ // First, scan the query tree for inferred type mappings (e.g. based on a comparison of those columns to some regular column
+ // with a type mapping).
+ var inferredColumns = new ColumnTypeMappingScanner().Scan(visited);
+
+ // Then, apply those type mappings back on the constant/parameter tables (e.g. ValuesExpression).
+ visited = ApplyInferredTypeMappings(visited, inferredColumns);
+ }
+
+ return visited;
+ }
+
///
protected override Expression VisitExtension(Expression extensionExpression)
{
@@ -83,6 +109,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 +149,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 +181,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
.Visit(shapedQueryExpression.ShaperExpression));
case SqlQueryRootExpression sqlQueryRootExpression:
+ {
var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType);
if (typeMapping == null)
{
@@ -161,8 +190,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
}
var selectExpression = new SelectExpression(
- sqlQueryRootExpression.Type, typeMapping,
- new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument));
+ new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument), SqlQuerySingleColumnAlias, sqlQueryRootExpression.Type, typeMapping);
Expression shaperExpression = new ProjectionBindingExpression(
selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable());
@@ -177,6 +205,20 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
}
return new ShapedQueryExpression(selectExpression, shaperExpression);
+ }
+
+ case InlineQueryRootExpression constantQueryRootExpression:
+ return VisitInlineQueryRoot(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 TranslateCollection(
+ sqlParameterExpression,
+ elementTypeMapping: null,
+ char.ToLowerInvariant(sqlParameterExpression.Name.First(c => c != '_')).ToString())
+ ?? base.VisitExtension(extensionExpression);
default:
return base.VisitExtension(extensionExpression);
@@ -212,7 +254,133 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
}
- return base.VisitMethodCall(methodCallExpression);
+ var translated = base.VisitMethodCall(methodCallExpression);
+
+ // Attempt to translate access into a primitive collection property
+ if (translated == QueryCompilationContext.NotTranslatedExpression
+ && _sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var propertyAccessExpression)
+ && propertyAccessExpression is
+ {
+ TypeMapping.ElementTypeMapping: RelationalTypeMapping elementTypeMapping
+ } collectionPropertyAccessExpression)
+ {
+ var tableAlias = collectionPropertyAccessExpression switch
+ {
+ ColumnExpression c => c.Name[..1].ToLowerInvariant(),
+ JsonScalarExpression { Path: [.., { PropertyName: string propertyName }] } => propertyName[..1].ToLowerInvariant(),
+ _ => "j"
+ };
+
+ if (TranslateCollection(collectionPropertyAccessExpression, elementTypeMapping, tableAlias) is
+ { } primitiveCollectionTranslation)
+ {
+ return primitiveCollectionTranslation;
+ }
+ }
+
+ return translated;
+ }
+
+ ///
+ /// Translates a parameter or column 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).
+ ///
+ ///
+ /// Inline collections aren't passed to this method; see for the translation of inline
+ /// collections.
+ ///
+ /// The expression to try to translate as a primitive collection expression.
+ ///
+ /// The type mapping of the collection's element, or when it's not known (i.e. for parameters).
+ ///
+ ///
+ /// Provides an alias to be used for the table returned from translation, which will represent the collection.
+ ///
+ /// A if the translation was successful, otherwise .
+ protected virtual ShapedQueryExpression? TranslateCollection(
+ SqlExpression sqlExpression,
+ RelationalTypeMapping? elementTypeMapping,
+ string tableAlias)
+ => null;
+
+ ///
+ /// Translates an inline collection into a queryable SQL VALUES expression.
+ ///
+ /// The inline collection to be translated.
+ /// A queryable SQL VALUES expression.
+ protected virtual ShapedQueryExpression? VisitInlineQueryRoot(InlineQueryRootExpression inlineQueryRootExpression)
+ {
+ var elementType = inlineQueryRootExpression.ElementType;
+
+ var rowExpressions = new List();
+ var encounteredNull = false;
+ var intTypeMapping = _typeMappingSource.FindMapping(typeof(int));
+
+ for (var i = 0; i < inlineQueryRootExpression.Values.Count; i++)
+ {
+ var value = inlineQueryRootExpression.Values[i];
+
+ // We currently support constants only; supporting non-constant values in VALUES is tracked by #30734.
+ if (value is not ConstantExpression constantExpression)
+ {
+ AddTranslationErrorDetails(RelationalStrings.OnlyConstantsSupportedInInlineCollectionQueryRoots);
+ return null;
+ }
+
+ if (constantExpression.Value is null)
+ {
+ encounteredNull = true;
+ }
+
+ rowExpressions.Add(new RowValueExpression(new[]
+ {
+ // Since VALUES may not guarantee row ordering, we add an _ord value by which we'll order.
+ _sqlExpressionFactory.Constant(i, intTypeMapping),
+ // Note that for the actual value, we must leave the type mapping null to allow it to get inferred later based on usage
+ _sqlExpressionFactory.Constant(constantExpression.Value, elementType, typeMapping: null)
+ }));
+ }
+
+ if (rowExpressions.Count == 0)
+ {
+ AddTranslationErrorDetails(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+ return null;
+ }
+
+ var valuesExpression = new ValuesExpression("v", rowExpressions, new[] { ValuesOrderingColumnName, ValuesValueColumnName });
+
+ // Note: we leave the element type mapping null, to allow it to get inferred based on queryable operators composed on top.
+ var selectExpression = new SelectExpression(
+ valuesExpression,
+ ValuesValueColumnName,
+ columnType: elementType.UnwrapNullableType(),
+ columnTypeMapping: null,
+ isColumnNullable: encounteredNull);
+
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ valuesExpression,
+ ValuesOrderingColumnName,
+ typeof(int),
+ intTypeMapping,
+ columnNullable: false),
+ ascending: true));
+
+ Expression shaperExpression = new ProjectionBindingExpression(
+ selectExpression, new ProjectionMember(), encounteredNull ? elementType.MakeNullable() : elementType);
+
+ if (elementType != shaperExpression.Type)
+ {
+ Check.DebugAssert(
+ elementType.MakeNullable() == shaperExpression.Type,
+ "expression.Type must be nullable of targetType");
+
+ shaperExpression = Expression.Convert(shaperExpression, elementType);
+ }
+
+ return new ShapedQueryExpression(selectExpression, shaperExpression);
}
///
@@ -244,7 +412,18 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
}
var selectExpression = (SelectExpression)source.QueryExpression;
- selectExpression.ApplyPredicate(_sqlExpressionFactory.Not(translation));
+
+ // Negate the predicate, unless it's already negated, in which case remove that.
+ selectExpression.ApplyPredicate(
+ translation is SqlUnaryExpression { OperatorType: ExpressionType.Not, Operand: var nestedOperand }
+ ? nestedOperand
+ : _sqlExpressionFactory.Not(translation));
+
+ if (TrySimplifyValuesToInExpression(source, isNegated: true, out var simplifiedQuery))
+ {
+ return simplifiedQuery;
+ }
+
selectExpression.ReplaceProjection(new List());
selectExpression.ApplyProjection();
if (selectExpression.Limit == null
@@ -273,6 +452,11 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
}
source = translatedSource;
+
+ if (TrySimplifyValuesToInExpression(source, isNegated: false, out var simplifiedQuery))
+ {
+ return simplifiedQuery;
+ }
}
var selectExpression = (SelectExpression)source.QueryExpression;
@@ -404,7 +588,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
return null;
}
- if (selectExpression.Orderings.Count == 0)
+ if (!IsOrdered(selectExpression))
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -952,7 +1136,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return null;
}
- if (selectExpression.Orderings.Count == 0)
+ if (!IsOrdered(selectExpression))
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -980,7 +1164,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return null;
}
- if (selectExpression.Orderings.Count == 0)
+ if (!IsOrdered(selectExpression))
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -1583,9 +1767,13 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate(
protected virtual SqlExpression? TranslateExpression(Expression expression)
{
var translation = _sqlTranslator.Translate(expression);
- if (translation == null && _sqlTranslator.TranslationErrorDetails != null)
+
+ if (translation is null)
{
- AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails);
+ if (_sqlTranslator.TranslationErrorDetails != null)
+ {
+ AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails);
+ }
}
return translation;
@@ -1602,6 +1790,111 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate(
LambdaExpression lambdaExpression)
=> TranslateExpression(RemapLambdaBody(shapedQueryExpression, lambdaExpression));
+ ///
+ /// Invoked at the end of top-level translation, applies inferred type mappings for queryable constants/parameters and verifies that
+ /// all have a type mapping.
+ ///
+ /// The query expression to process.
+ ///
+ /// Inferred type mappings for queryable constants/parameters collected during translation. These will be applied to the appropriate
+ /// nodes in the tree.
+ ///
+ protected virtual Expression ApplyInferredTypeMappings(
+ Expression expression,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => new RelationalInferredTypeMappingApplier(inferredTypeMappings).Visit(expression);
+
+ ///
+ /// Determines whether the given is ordered, typically because orderings have been added to it.
+ ///
+ /// The to check for ordering.
+ /// Whether is ordered.
+ protected virtual bool IsOrdered(SelectExpression selectExpression)
+ => selectExpression.Orderings.Count > 0;
+
+ ///
+ /// Attempts to pattern-match for Contains over , which corresponds to
+ /// Where(b => new[] { 1, 2, 3 }.Contains(b.Id)). Simplifies this to the tighter [b].[Id] IN (1, 2, 3) instead of the
+ /// full subquery with VALUES.
+ ///
+ private bool TrySimplifyValuesToInExpression(
+ ShapedQueryExpression source,
+ bool isNegated,
+ [NotNullWhen(true)] out ShapedQueryExpression? simplifiedQuery)
+ {
+ if (source.QueryExpression is SelectExpression
+ {
+ Tables: [ValuesExpression
+ {
+ RowValues: [{ Values.Count: 2 }, ..],
+ ColumnNames: [ ValuesOrderingColumnName, ValuesValueColumnName ]
+ } valuesExpression],
+ GroupBy: [],
+ Having: null,
+ IsDistinct: false,
+ Limit: null,
+ Offset: null,
+ // Note that we don't care about orderings, they get elided anyway by Any/All
+ Predicate: SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: var left, Right: var right },
+ } selectExpression)
+ {
+ // The table is a ValuesExpression, and the predicate is an equality - this is a possible simplifiable Contains.
+ // Get the projection column pointing to the ValuesExpression, and check that it's compared to on one side of the predicate
+ // equality.
+ var shaperExpression = source.ShaperExpression;
+ if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression
+ && unaryExpression.Operand.Type.IsNullableType()
+ && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type)
+ {
+ shaperExpression = unaryExpression.Operand;
+ }
+
+ if (shaperExpression is ProjectionBindingExpression projectionBindingExpression
+ && selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression projectionColumn)
+ {
+ SqlExpression item;
+
+ if (left is ColumnExpression leftColumn
+ && (leftColumn.Table, leftColumn.Name) == (projectionColumn.Table, projectionColumn.Name))
+ {
+ item = right;
+ }
+ else if (right is ColumnExpression rightColumn
+ && (rightColumn.Table, rightColumn.Name) == (projectionColumn.Table, projectionColumn.Name))
+ {
+ item = left;
+ }
+ else
+ {
+ simplifiedQuery = null;
+ return false;
+ }
+
+ var values = new object?[valuesExpression.RowValues.Count];
+ for (var i = 0; i < values.Length; i++)
+ {
+ // Skip the first value (_ord), which is irrelevant for Contains
+ if (valuesExpression.RowValues[i].Values[1] is SqlConstantExpression { Value: var constantValue })
+ {
+ values[i] = constantValue;
+ }
+ else
+ {
+ simplifiedQuery = null;
+ return false;
+ }
+ }
+
+ var inExpression = _sqlExpressionFactory.In(item, _sqlExpressionFactory.Constant(values), isNegated);
+ simplifiedQuery = source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression);
+ return true;
+ }
+ }
+
+ simplifiedQuery = null;
+ return false;
+ }
+
private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
{
var lambdaBody = ReplacingExpressionVisitor.Replace(
@@ -2275,4 +2568,299 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape
return source.UpdateShaperExpression(shaper);
}
+
+ ///
+ /// A visitor which scans an expression tree and attempts to find columns for which we were missing type mappings (projected out
+ /// of queryable constant/parameter), and those type mappings have been inferred.
+ ///
+ ///
+ ///
+ /// This handles two cases: (1) an untyped column which type-inferred in the regular way, e.g. through comparison to a typed
+ /// column, and (2) set operations where on side is typed and the other is untyped.
+ ///
+ ///
+ /// Note that this visitor follows type columns across subquery projections. That is, if a root constant/parameter is buried
+ /// within subqueries, and somewhere above the column projected out of a subquery is inferred, this is picked up and propagated
+ /// all the way down.
+ ///
+ ///
+ /// The visitor dose not change the query tree in any way - it only populates the inferred type mappings it identified in
+ /// the given dictionary; actual application of the inferred type mappings happens later in
+ /// . We can't do this in a single pass since untyped roots
+ /// (e.g. may get visited before the type-inferred column referring to them (e.g. CROSS APPLY,
+ /// correlated subquery).
+ ///
+ ///
+ private sealed class ColumnTypeMappingScanner : ExpressionVisitor
+ {
+ private readonly Dictionary<(TableExpressionBase, string), RelationalTypeMapping> _inferredColumns = new();
+
+ private SelectExpression? _currentSelectExpression;
+ private ProjectionExpression? _currentProjectionExpression;
+
+ public IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> Scan(Expression expression)
+ {
+ _inferredColumns.Clear();
+
+ Visit(expression);
+
+ return _inferredColumns;
+ }
+
+ protected override Expression VisitExtension(Expression node)
+ {
+ switch (node)
+ {
+ // A column on a table which was possibly originally untyped (constant/parameter root or a subquery projection of one),
+ // which now does have a type mapping - this would mean in got inferred in the usual manner (comparison with typed column).
+ // Registered the inferred type mapping so it can be later applied back to its table, if it's untyped.
+ case ColumnExpression { TypeMapping: { } typeMapping } c when WasMaybeOriginallyUntyped(c):
+ {
+ RegisterInferredTypeMapping(c, typeMapping);
+
+ return base.VisitExtension(node);
+ }
+
+ // Similar to the above, but with ScalarSubqueryExpression the inferred type mapping is on the expression itself, while the
+ // ColumnExpression we need is on the subquery's projection.
+ case ScalarSubqueryExpression
+ {
+ TypeMapping: { } typeMapping,
+ Subquery.Projection: [{ Expression: ColumnExpression columnExpression }]
+ }
+ when WasMaybeOriginallyUntyped(columnExpression):
+ {
+ RegisterInferredTypeMapping(columnExpression, typeMapping);
+
+ return base.VisitExtension(node);
+ }
+
+ // For set operations involving a leg with a type mapping (e.g. some column) and a leg without one (queryable constant or
+ // parameter), we infer the missing type mapping from the other side.
+ case SetOperationBase
+ {
+ Source1.Projection: [{ Expression: var projection1 }],
+ Source2.Projection: [{ Expression: var projection2 }]
+ }
+ when UnwrapConvert(projection1) is ColumnExpression column1 && UnwrapConvert(projection2) is ColumnExpression column2:
+ {
+ if (projection1.TypeMapping is not null && WasMaybeOriginallyUntyped(column2))
+ {
+ RegisterInferredTypeMapping(column2, projection1.TypeMapping);
+ }
+
+ if (projection2.TypeMapping is not null && WasMaybeOriginallyUntyped(column1))
+ {
+ RegisterInferredTypeMapping(column1, projection2.TypeMapping);
+ }
+
+ return base.VisitExtension(node);
+ }
+
+ // Record state on the SelectExpression and ProjectionExpression so that we can associate ColumnExpressions to the
+ // projections they're in (see below).
+ case SelectExpression selectExpression:
+ {
+ var parentSelectExpression = _currentSelectExpression;
+ _currentSelectExpression = selectExpression;
+ var visited = base.VisitExtension(selectExpression);
+ _currentSelectExpression = parentSelectExpression;
+ return visited;
+ }
+
+ case ProjectionExpression projectionExpression:
+ {
+ var parentProjectionExpression = _currentProjectionExpression;
+ _currentProjectionExpression = projectionExpression;
+ var visited = base.VisitExtension(projectionExpression);
+ _currentProjectionExpression = parentProjectionExpression;
+ return visited;
+ }
+
+ // When visiting subqueries, we want to propagate the inferred type mappings from above into the subquery, recursively.
+ // So we record state above to know which subquery and projection we're visiting; when visiting columns inside a projection
+ // which has an inferred type mapping from above, we register the inferred type mapping for that column too.
+ case ColumnExpression { TypeMapping: null } columnExpression
+ when _currentSelectExpression is not null
+ && _currentProjectionExpression is not null
+ && _inferredColumns.TryGetValue(
+ (_currentSelectExpression, _currentProjectionExpression.Alias), out var inferredTypeMapping)
+ && WasMaybeOriginallyUntyped(columnExpression):
+ {
+ RegisterInferredTypeMapping(columnExpression, inferredTypeMapping);
+ return base.VisitExtension(node);
+ }
+
+ case ShapedQueryExpression shapedQueryExpression:
+ return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression));
+
+ default:
+ return base.VisitExtension(node);
+ }
+
+ bool WasMaybeOriginallyUntyped(ColumnExpression columnExpression)
+ {
+ var underlyingTable = columnExpression.Table is JoinExpressionBase joinExpression
+ ? joinExpression.Table
+ : columnExpression.Table;
+
+ return underlyingTable switch
+ {
+ TableExpression
+ => false,
+
+ SelectExpression subquery
+ => subquery.Projection.FirstOrDefault(p => p.Alias == columnExpression.Name) is { Expression.TypeMapping: null },
+
+ JoinExpressionBase
+ => throw new InvalidOperationException("Impossible: nested join"),
+
+ // Any other table expression is considered a root (TableValuedFunctionExpression, ValuesExpression...) which *may* be
+ // untyped, so we record the possible inference (note that TableValuedFunctionExpression may be typed, or not)
+ _ => true,
+ };
+ }
+
+ SqlExpression UnwrapConvert(SqlExpression expression)
+ => expression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } convert
+ ? UnwrapConvert(convert.Operand)
+ : expression;
+ }
+
+ private void RegisterInferredTypeMapping(ColumnExpression columnExpression, RelationalTypeMapping inferredTypeMapping)
+ {
+ var underlyingTable = columnExpression.Table is JoinExpressionBase joinExpression
+ ? joinExpression.Table
+ : columnExpression.Table;
+
+ if (_inferredColumns.TryGetValue((underlyingTable, columnExpression.Name), out var knownTypeMapping)
+ && inferredTypeMapping != knownTypeMapping)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.ConflictingTypeMappingsForPrimitiveCollection(
+ inferredTypeMapping.StoreType, knownTypeMapping.StoreType));
+ }
+
+ _inferredColumns[(underlyingTable, columnExpression.Name)] = inferredTypeMapping;
+ }
+ }
+
+ ///
+ /// A visitor executed at the end of translation, which verifies that all nodes have a type mapping,
+ /// and applies type mappings inferred for queryable constants (VALUES) and parameters (e.g. OPENJSON) back on their root tables.
+ ///
+ protected class RelationalInferredTypeMappingApplier : ExpressionVisitor
+ {
+ private SelectExpression? _currentSelectExpression;
+
+ ///
+ /// The inferred type mappings to be applied back on their query roots.
+ ///
+ protected IReadOnlyDictionary<(TableExpressionBase Table, string ColumnName), RelationalTypeMapping> InferredTypeMappings { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The inferred type mappings to be applied back on their query roots.
+ public RelationalInferredTypeMappingApplier(
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => InferredTypeMappings = inferredTypeMappings;
+
+ ///
+ protected override Expression VisitExtension(Expression expression)
+ {
+ switch (expression)
+ {
+ case ColumnExpression { TypeMapping: null } columnExpression
+ when InferredTypeMappings.TryGetValue((columnExpression.Table, columnExpression.Name), out var typeMapping):
+ return columnExpression.ApplyTypeMapping(typeMapping);
+
+ case SelectExpression selectExpression:
+ var parentSelectExpression = _currentSelectExpression;
+ _currentSelectExpression = selectExpression;
+ var visited = base.VisitExtension(expression);
+ _currentSelectExpression = parentSelectExpression;
+ return visited;
+
+ // For ValueExpression, apply the inferred type mapping on all constants inside.
+ case ValuesExpression valuesExpression:
+ // By default, the ValuesExpression also contains an ordering by a synthetic increasing _ord. If the containing
+ // SelectExpression doesn't project it out or require it (limit/offset), strip that out.
+ // TODO: Strictly-speaking, this doesn't belong in this visitor which is about applying type mappings
+ return ApplyTypeMappingsOnValuesExpression(
+ valuesExpression,
+ stripOrdering: _currentSelectExpression is { Limit: null, Offset: null }
+ && !_currentSelectExpression.Projection.Any(
+ p => p.Expression is ColumnExpression { Name: ValuesOrderingColumnName } c && c.Table == valuesExpression));
+
+ // SqlExpressions without an inferred type mapping indicates a problem in EF - everything should have been inferred.
+ // One exception is SqlFragmentExpression, which never has a type mapping.
+ case SqlExpression { TypeMapping: null } sqlExpression and not SqlFragmentExpression and not ColumnExpression:
+ throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print()));
+
+ case ShapedQueryExpression shapedQueryExpression:
+ return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression));
+
+ default:
+ return base.VisitExtension(expression);
+ }
+ }
+
+ ///
+ /// Applies the given type mappings to the values projected out by the given .
+ /// As an optimization, it can also strip the first _ord column if it's determined that it isn't needed (most cases).
+ ///
+ /// The to apply the mappings to.
+ /// Whether to strip the _ord column.
+ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExpression valuesExpression, bool stripOrdering)
+ {
+ var inferredTypeMappings = InferredTypeMappings.TryGetValue((valuesExpression, ValuesValueColumnName), out var typeMapping)
+ ? new[] { null, typeMapping }
+ : new RelationalTypeMapping?[] { null, null };
+
+ Check.DebugAssert(
+ valuesExpression.ColumnNames[0] == ValuesOrderingColumnName, "First ValuesExpression column isn't the ordering column");
+ var newColumnNames = stripOrdering
+ ? valuesExpression.ColumnNames.Skip(1).ToArray()
+ : valuesExpression.ColumnNames;
+
+ var newRowValues = new RowValueExpression[valuesExpression.RowValues.Count];
+ for (var i = 0; i < newRowValues.Length; i++)
+ {
+ var rowValue = valuesExpression.RowValues[i];
+ var newValues = new SqlExpression[newColumnNames.Count];
+ for (var j = 0; j < valuesExpression.ColumnNames.Count; j++)
+ {
+ Check.DebugAssert(rowValue.Values[j] is SqlConstantExpression, "Non-constant SqlExpression in ValuesExpression");
+
+ if (j == 0 && stripOrdering)
+ {
+ continue;
+ }
+
+ var value = (SqlConstantExpression)rowValue.Values[j];
+ SqlExpression newValue = value;
+
+ var inferredTypeMapping = inferredTypeMappings[j];
+ if (inferredTypeMapping is not null && value.TypeMapping is null)
+ {
+ newValue = new SqlConstantExpression(Expression.Constant(value.Value, value.Type), inferredTypeMapping);
+
+ // We currently add explicit conversions on the first row, to ensure that the inferred types are properly typed.
+ // See #30605 for removing that when not needed.
+ if (i == 0)
+ {
+ newValue = new SqlUnaryExpression(ExpressionType.Convert, newValue, newValue.Type, newValue.TypeMapping);
+ }
+ }
+
+ newValues[j - (stripOrdering ? 1 : 0)] = newValue;
+ }
+
+ newRowValues[i] = new RowValueExpression(newValues);
+ }
+
+ return new(valuesExpression.Alias, newRowValues, newColumnNames, valuesExpression.GetAnnotations());
+ }
+ }
}
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index 737d5fd13c3..38569b6d852 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -66,7 +66,6 @@ private static readonly MethodInfo StringEqualsWithStringComparisonStatic
private readonly IModel _model;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly QueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor;
- private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlTypeMappingVerifyingExpressionVisitor;
private bool _throwForNotTranslatedEfProperty;
@@ -86,7 +85,6 @@ public RelationalSqlTranslatingExpressionVisitor(
_queryCompilationContext = queryCompilationContext;
_model = queryCompilationContext.Model;
_queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor;
- _sqlTypeMappingVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor();
_throwForNotTranslatedEfProperty = true;
}
@@ -149,8 +147,6 @@ protected virtual void AddTranslationErrorDetails(string details)
return null;
}
- _sqlTypeMappingVerifyingExpressionVisitor.Visit(translation);
-
return translation;
}
@@ -704,6 +700,12 @@ protected override Expression VisitExtension(Expression extensionExpression)
return scalarSubqueryExpression;
+ // We have e.g. an array parameter inside a Where clause; this is represented as a QueryableParameterQueryRootExpression so
+ // that we can translate queryable operators over it (query root in subquery context), but in normal SQL translation context
+ // we just unwrap the query root expression to get the parameter out.
+ case ParameterQueryRootExpression queryableParameterQueryRootExpression:
+ return Visit(queryableParameterQueryRootExpression.ParameterExpression);
+
default:
return QueryCompilationContext.NotTranslatedExpression;
}
@@ -738,6 +740,36 @@ protected override Expression VisitMember(MemberExpression memberExpression)
protected override Expression VisitMemberInit(MemberInitExpression memberInitExpression)
=> GetConstantOrNotTranslated(memberInitExpression);
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public virtual bool TryTranslatePropertyAccess(Expression expression, [NotNullWhen(true)] out SqlExpression? propertyAccessExpression)
+ {
+ if (expression is MethodCallExpression methodCallExpression)
+ {
+ if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var propertyName)
+ && TryBindMember(Visit(source), MemberIdentity.Create(propertyName)) is { } result)
+ {
+ propertyAccessExpression = result;
+ return true;
+ }
+
+ if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName)
+ && TryBindMember(Visit(source), MemberIdentity.Create(propertyName)) is { } indexerResult)
+ {
+ propertyAccessExpression = indexerResult;
+ return true;
+ }
+ }
+
+ propertyAccessExpression = null;
+ return false;
+ }
+
///
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
@@ -1913,13 +1945,4 @@ public Expression Convert(Type type)
: new EntityReferenceExpression(this, derivedEntityType);
}
}
-
- private sealed class SqlTypeMappingVerifyingExpressionVisitor : ExpressionVisitor
- {
- protected override Expression VisitExtension(Expression extensionExpression)
- => extensionExpression is SqlExpression { TypeMapping: null } sqlExpression
- && extensionExpression is not SqlFragmentExpression
- ? throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print()))
- : base.VisitExtension(extensionExpression);
- }
}
diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
index 1da3ecfb20c..6dc46ca1058 100644
--- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
@@ -57,9 +57,11 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping),
CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping),
CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping),
+ ColumnExpression e => e.ApplyTypeMapping(typeMapping),
DistinctExpression e => ApplyTypeMappingOnDistinct(e, typeMapping),
InExpression e => ApplyTypeMappingOnIn(e),
LikeExpression e => ApplyTypeMappingOnLike(e),
+ ScalarSubqueryExpression e => e.ApplyTypeMapping(typeMapping),
SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping),
SqlConstantExpression e => e.ApplyTypeMapping(typeMapping),
SqlFragmentExpression e => e,
@@ -641,8 +643,9 @@ private void AddConditions(SelectExpression selectExpression, IEntityType entity
return;
}
- var firstTable = selectExpression.Tables[0];
- var table = (firstTable as FromSqlExpression)?.Table ?? ((ITableBasedExpression)firstTable).Table;
+ var table = (selectExpression.Tables[0] as ITableBasedExpression)?.Table;
+ Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
+
if (table.IsOptional(entityType))
{
SqlExpression? predicate = null;
diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
index 46fdcdb0e10..bb4c44c9307 100644
--- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
@@ -43,6 +43,7 @@ ShapedQueryExpression shapedQueryExpression
ProjectionExpression projectionExpression => VisitProjection(projectionExpression),
TableValuedFunctionExpression tableValuedFunctionExpression => VisitTableValuedFunction(tableValuedFunctionExpression),
RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression),
+ RowValueExpression rowValueExpression => VisitRowValue(rowValueExpression),
ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression),
SelectExpression selectExpression => VisitSelect(selectExpression),
SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression),
@@ -55,6 +56,7 @@ ShapedQueryExpression shapedQueryExpression
UnionExpression unionExpression => VisitUnion(unionExpression),
UpdateExpression updateExpression => VisitUpdate(updateExpression),
JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression),
+ ValuesExpression valuesExpression => VisitValues(valuesExpression),
_ => base.VisitExtension(extensionExpression),
};
@@ -205,6 +207,13 @@ ShapedQueryExpression shapedQueryExpression
/// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
protected abstract Expression VisitRowNumber(RowNumberExpression rowNumberExpression);
+ ///
+ /// Visits the children of the row value expression.
+ ///
+ /// The expression to visit.
+ /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
+ protected abstract Expression VisitRowValue(RowValueExpression rowValueExpression);
+
///
/// Visits the children of the scalar subquery expression.
///
@@ -288,4 +297,11 @@ ShapedQueryExpression shapedQueryExpression
/// The expression to visit.
/// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
protected abstract Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression);
+
+ ///
+ /// Visits the children of the values expression.
+ ///
+ /// The expression to visit.
+ /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
+ protected abstract Expression VisitValues(ValuesExpression valuesExpression);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs
index 2a9a10cfb23..7d63dc2d58d 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs
@@ -51,6 +51,13 @@ protected ColumnExpression(Type type, RelationalTypeMapping? typeMapping)
/// A new expression which has property set to true.
public abstract ColumnExpression MakeNullable();
+ ///
+ /// Applies supplied type mapping to this expression.
+ ///
+ /// A relational type mapping to apply.
+ /// A new expression which has supplied type mapping.
+ public abstract SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping);
+
///
protected override void Print(ExpressionPrinter expressionPrinter)
{
diff --git a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
index f51942ea361..b82c4944f42 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
@@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
/// not used in application code.
///
///
-public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBase
+public class FromSqlExpression : TableExpressionBase, ITableBasedExpression, IClonableTableExpressionBase
{
///
/// Creates a new instance of the class.
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs
index 06eecbdab27..7254b9c5f08 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs
@@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
///
///
-/// An interface that gives access to associated with given table source.
+/// An interface that gives access to an optional associated with given table source.
///
///
/// This type is typically used by database providers (and other extensions). It is generally
@@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
public interface ITableBasedExpression
{
///
- /// The associated with given table source.
+ /// The associated with given table source, if any.
///
- ITableBase Table { get; }
+ ITableBase? Table { get; }
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs
new file mode 100644
index 00000000000..ecee8a57cff
--- /dev/null
+++ b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs
@@ -0,0 +1,146 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Data;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+///
+///
+/// An expression that represents a SQL row.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class RowValueExpression : SqlExpression
+{
+ ///
+ /// The values of this row.
+ ///
+ public virtual IReadOnlyList Values { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The values of this row.
+ public RowValueExpression(IReadOnlyList values)
+ : base(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,14 @@ 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 virtual SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
+ => 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 0af3351d300..defc4f156c1 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
@@ -605,7 +605,7 @@ public ConcreteColumnExpression(
string name,
TableReferenceExpression table,
Type type,
- RelationalTypeMapping typeMapping,
+ RelationalTypeMapping? typeMapping,
bool nullable)
: base(type, typeMapping)
{
@@ -629,7 +629,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);
@@ -1019,12 +1022,11 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor
newArguments[i] = (SqlExpression)Visit(tableValuedFunctionExpression.Arguments[i]);
}
- var newTableValuedFunctionExpression = new TableValuedFunctionExpression(
- tableValuedFunctionExpression.StoreFunction,
- newArguments)
- {
- Alias = tableValuedFunctionExpression.Alias
- };
+ var newTableValuedFunctionExpression = tableValuedFunctionExpression.StoreFunction is null
+ ? new TableValuedFunctionExpression(
+ tableValuedFunctionExpression.Alias, tableValuedFunctionExpression.Name, newArguments)
+ : new TableValuedFunctionExpression(
+ tableValuedFunctionExpression.StoreFunction, newArguments) { Alias = tableValuedFunctionExpression.Alias };
foreach (var annotation in tableValuedFunctionExpression.GetAnnotations())
{
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index e39a89f4ad5..547535b4da6 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -25,7 +25,6 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
public sealed partial class SelectExpression : TableExpressionBase
{
private const string DiscriminatorColumnAlias = "Discriminator";
- private const string SqlQuerySingleColumnAlias = "Value";
private static readonly IdentifierComparer IdentifierComparerInstance = new();
private static readonly Dictionary MirroredOperationMap =
@@ -112,14 +111,32 @@ internal SelectExpression(SqlExpression? projection)
}
}
- internal SelectExpression(Type type, RelationalTypeMapping typeMapping, FromSqlExpression fromSqlExpression)
+ ///
+ /// Creates a new instance of the class given a , with a single
+ /// column projection.
+ ///
+ /// The table expression.
+ /// The name of the column to add as the projection.
+ /// The type of the column to add as the projection.
+ /// The type mapping of the column to add as the projection.
+ /// Whether the column projected out is nullable.
+ public SelectExpression(
+ TableExpressionBase tableExpression,
+ string columnName,
+ Type columnType,
+ RelationalTypeMapping? columnTypeMapping,
+ bool? isColumnNullable = null)
: base(null)
{
- var tableReferenceExpression = new TableReferenceExpression(this, fromSqlExpression.Alias!);
- AddTable(fromSqlExpression, tableReferenceExpression);
+ var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!);
+ AddTable(tableExpression, tableReferenceExpression);
var columnExpression = new ConcreteColumnExpression(
- SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType());
+ columnName,
+ tableReferenceExpression,
+ columnType.UnwrapNullableType(),
+ columnTypeMapping,
+ isColumnNullable ?? columnType.IsNullableType());
_projectionMapping[new ProjectionMember()] = columnExpression;
}
@@ -466,7 +483,9 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
throw new InvalidOperationException(RelationalStrings.SelectExpressionNonTphWithCustomTable(entityType.DisplayName()));
}
- var table = (tableExpressionBase as FromSqlExpression)?.Table ?? ((ITableBasedExpression)tableExpressionBase).Table;
+ var table = (tableExpressionBase as ITableBasedExpression)?.Table;
+ Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
+
var tableReferenceExpression = new TableReferenceExpression(this, tableExpressionBase.Alias!);
AddTable(tableExpressionBase, tableReferenceExpression);
@@ -3912,6 +3931,32 @@ public void PrepareForAggregate()
}
}
+ ///
+ /// Creates a that references a table on this .
+ ///
+ /// The table expression referenced by the column.
+ /// The column name.
+ /// The column CLR type.
+ /// The column's type mapping.
+ /// Whether the column is nullable.
+ public ColumnExpression CreateColumnExpression(
+ TableExpressionBase tableExpression,
+ string columnName,
+ Type type,
+ RelationalTypeMapping? typeMapping,
+ bool? columnNullable = null)
+ {
+ var tableIndex = _tables.FindIndex(teb => ReferenceEquals(teb, tableExpression));
+ var tableReferenceExpression = _tableReferences[tableIndex];
+
+ return new ConcreteColumnExpression(
+ columnName,
+ tableReferenceExpression,
+ type.UnwrapNullableType(),
+ typeMapping,
+ columnNullable ?? type.IsNullableType());
+ }
+
///
/// 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.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
index 888e1bbe5ac..b4869e373dc 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
@@ -24,20 +24,66 @@ public class TableValuedFunctionExpression : TableExpressionBase, ITableBasedExp
public TableValuedFunctionExpression(IStoreFunction storeFunction, IReadOnlyList arguments)
: this(
storeFunction.Name[..1].ToLowerInvariant(),
- storeFunction,
+ storeFunction.Name,
+ storeFunction.Schema,
+ storeFunction.IsBuiltIn,
arguments,
annotations: null)
+ {
+ StoreFunction = storeFunction;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The name of the function.
+ /// The arguments of the function.
+ /// A collection of annotations associated with this expression.
+ public TableValuedFunctionExpression(
+ string name,
+ IReadOnlyList arguments,
+ IEnumerable? annotations = null)
+ : this(name[..1].ToLowerInvariant(), name, schema: null, builtIn: true, arguments, annotations)
{
}
- private TableValuedFunctionExpression(
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// The name of the function.
+ /// The arguments of the function.
+ /// A collection of annotations associated with this expression.
+ public TableValuedFunctionExpression(
+ string alias,
+ string name,
+ IReadOnlyList arguments,
+ IEnumerable? annotations = null)
+ : this(alias, name, schema: null, builtIn: true, arguments, annotations)
+ {
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// The name of the function.
+ /// The schema of the function.
+ /// Whether the function is built-in.
+ /// The arguments of the function.
+ /// A collection of annotations associated with this expression.
+ protected TableValuedFunctionExpression(
string alias,
- IStoreFunction storeFunction,
+ string name,
+ string? schema,
+ bool builtIn,
IReadOnlyList arguments,
- IEnumerable? annotations)
+ IEnumerable? annotations = null)
: base(alias, annotations)
{
- StoreFunction = storeFunction;
+ Name = name;
+ Schema = schema;
+ IsBuiltIn = builtIn;
Arguments = arguments;
}
@@ -54,17 +100,32 @@ public override string? Alias
///
/// The store function.
///
- public virtual IStoreFunction StoreFunction { get; }
+ public virtual IStoreFunction? StoreFunction { get; }
+
+ ///
+ ITableBase? ITableBasedExpression.Table
+ => StoreFunction;
+
+ ///
+ /// The name of the function.
+ ///
+ public virtual string Name { get; }
+
+ ///
+ /// The schema of the function.
+ ///
+ public virtual string? Schema { get; }
+
+ ///
+ /// Gets the value indicating whether the function is built-in.
+ ///
+ public virtual bool IsBuiltIn { get; }
///
/// The list of arguments of this function.
///
public virtual IReadOnlyList Arguments { get; }
- ///
- ITableBase ITableBasedExpression.Table
- => StoreFunction;
-
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
@@ -77,7 +138,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 +150,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,6 +183,9 @@ public override bool Equals(object? obj)
private bool Equals(TableValuedFunctionExpression tableValuedFunctionExpression)
=> base.Equals(tableValuedFunctionExpression)
+ && Name == tableValuedFunctionExpression.Name
+ && Schema == tableValuedFunctionExpression.Schema
+ && IsBuiltIn == tableValuedFunctionExpression.IsBuiltIn
&& StoreFunction == tableValuedFunctionExpression.StoreFunction
&& Arguments.SequenceEqual(tableValuedFunctionExpression.Arguments);
@@ -130,6 +194,9 @@ public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(base.GetHashCode());
+ hash.Add(Name);
+ hash.Add(Schema);
+ hash.Add(IsBuiltIn);
hash.Add(StoreFunction);
for (var i = 0; i < Arguments.Count; 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..56fcde09c15
--- /dev/null
+++ b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
@@ -0,0 +1,181 @@
+// 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, IClonableTableExpressionBase
+{
+ ///
+ /// 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
+ }))
+ {
+ // See #30734 for non-constants
+ 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);
+
+ ///
+ public virtual TableExpressionBase Clone()
+ => CreateWithAnnotations(GetAnnotations());
+
+ ///
+ 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..bf39f8f5947 100644
--- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
+++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
@@ -185,6 +185,33 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB
case OuterApplyExpression outerApplyExpression:
return outerApplyExpression.Update(Visit(outerApplyExpression.Table));
+ case ValuesExpression valuesExpression:
+ {
+ RowValueExpression[]? newRowValues = null;
+
+ for (var i = 0; i < valuesExpression.RowValues.Count; i++)
+ {
+ var rowValue = valuesExpression.RowValues[i];
+ var newRowValue = (RowValueExpression)VisitRowValue(rowValue, allowOptimizedExpansion: false, out _);
+
+ if (newRowValue != rowValue && newRowValues is null)
+ {
+ newRowValues = new RowValueExpression[valuesExpression.RowValues.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newRowValues[j] = valuesExpression.RowValues[j];
+ }
+ }
+
+ if (newRowValues is not null)
+ {
+ newRowValues[i] = newRowValue;
+ }
+ }
+
+ return newRowValues is null ? valuesExpression : valuesExpression.Update(newRowValues);
+ }
+
case SelectExpression selectExpression:
return Visit(selectExpression);
@@ -403,6 +430,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
@@ -733,7 +762,7 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
case SqlParameterExpression sqlParameter:
DoNotCache();
typeMapping = sqlParameter.TypeMapping;
- values = (IEnumerable?)ParameterValues[sqlParameter.Name] ?? throw new NullReferenceException();
+ values = (IEnumerable?)ParameterValues[sqlParameter.Name] ?? Array.Empty();
break;
default:
@@ -826,6 +855,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 08fd2368865..536f658af60 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
@@ -120,6 +120,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
@@ -127,8 +128,10 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd(p => p.GetRequiredService())
.TryAddProviderSpecificServices(
b => b
+ .TryAddSingleton()
.TryAddSingleton()
.TryAddSingleton()
.TryAddSingleton()
diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/ISqlServerSingletonOptions.cs b/src/EFCore.SqlServer/Infrastructure/Internal/ISqlServerSingletonOptions.cs
new file mode 100644
index 00000000000..f5f661d65d3
--- /dev/null
+++ b/src/EFCore.SqlServer/Infrastructure/Internal/ISqlServerSingletonOptions.cs
@@ -0,0 +1,29 @@
+// 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.Infrastructure.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 interface ISqlServerSingletonOptions : ISingletonOptions
+{
+ ///
+ /// 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.
+ ///
+ int CompatibilityLevel { 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.
+ ///
+ int? CompatibilityLevelWithoutDefault { get; }
+}
diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerOptionsExtension.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerOptionsExtension.cs
index 30b59528aa5..50c0d07e894 100644
--- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerOptionsExtension.cs
+++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerOptionsExtension.cs
@@ -14,6 +14,21 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
public class SqlServerOptionsExtension : RelationalOptionsExtension
{
private DbContextOptionsExtensionInfo? _info;
+ private int? _compatibilityLevel;
+
+ ///
+ /// 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.
+ ///
+ // See https://learn.microsoft.com/sql/t-sql/statements/alter-database-transact-sql-compatibility-level
+ // SQL Server 2022 (16.x): compatibility level 160, start date 2022-11-16, mainstream end date 2028-01-11, extended end date 2033-01-11
+ // SQL Server 2019 (15.x): compatibility level 150, start date 2019-11-04, mainstream end date 2025-02-28, extended end date 2030-01-08
+ // SQL Server 2017 (14.x): compatibility level 140, start date 2017-09-29, mainstream end date 2022-10-11, extended end date 2027-10-12
+ // SQL Server 2016 (13.x): compatibility level 130, start date 2016-06-01, mainstream end date 2021-07-13, extended end date 2026-07-14
+ // SQL Server 2014 (12.x): compatibility level 120, start date 2014-06-05, mainstream end date 2019-07-09, extended end date 2024-07-09
+ public static readonly int DefaultCompatibilityLevel = 160;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -36,6 +51,7 @@ public SqlServerOptionsExtension()
protected SqlServerOptionsExtension(SqlServerOptionsExtension copyFrom)
: base(copyFrom)
{
+ _compatibilityLevel = copyFrom._compatibilityLevel;
}
///
@@ -56,6 +72,39 @@ public override DbContextOptionsExtensionInfo Info
protected override RelationalOptionsExtension Clone()
=> new SqlServerOptionsExtension(this);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual int CompatibilityLevel
+ => _compatibilityLevel ?? DefaultCompatibilityLevel;
+
+ ///
+ /// 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 int? CompatibilityLevelWithoutDefault
+ => _compatibilityLevel;
+
+ ///
+ /// 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 SqlServerOptionsExtension WithCompatibilityLevel(int? compatibilityLevel)
+ {
+ var clone = (SqlServerOptionsExtension)Clone();
+
+ clone._compatibilityLevel = compatibilityLevel;
+
+ return clone;
+ }
+
///
/// 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
@@ -81,7 +130,8 @@ public override bool IsDatabaseProvider
=> true;
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
- => other is ExtensionInfo;
+ => other is ExtensionInfo otherInfo
+ && Extension.CompatibilityLevel == otherInfo.Extension.CompatibilityLevel;
public override string LogFragment
{
@@ -93,6 +143,13 @@ public override string LogFragment
builder.Append(base.LogFragment);
+ if (Extension._compatibilityLevel is int compatibilityLevel)
+ {
+ builder
+ .Append("CompatibilityLevel=")
+ .Append(compatibilityLevel);
+ }
+
_logFragment = builder.ToString();
}
@@ -101,6 +158,13 @@ public override string LogFragment
}
public override void PopulateDebugInfo(IDictionary debugInfo)
- => debugInfo["SqlServer"] = "1";
+ {
+ debugInfo["SqlServer"] = "1";
+
+ if (Extension.CompatibilityLevel is int compatibilityLevel)
+ {
+ debugInfo["CompatibilityLevel"] = compatibilityLevel.ToString();
+ }
+ }
}
}
diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerSingletonOptions.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerSingletonOptions.cs
new file mode 100644
index 00000000000..dacfcb697b6
--- /dev/null
+++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerSingletonOptions.cs
@@ -0,0 +1,66 @@
+// 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.Infrastructure.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 SqlServerSingletonOptions : ISqlServerSingletonOptions
+{
+ ///
+ /// 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 int CompatibilityLevel { get; private set; } = SqlServerOptionsExtension.DefaultCompatibilityLevel;
+
+ ///
+ /// 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 int? CompatibilityLevelWithoutDefault { get; private set; }
+
+ ///
+ /// 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 void Initialize(IDbContextOptions options)
+ {
+ var sqlServerOptions = options.FindExtension();
+ if (sqlServerOptions != null)
+ {
+ CompatibilityLevel = sqlServerOptions.CompatibilityLevel;
+ CompatibilityLevelWithoutDefault = sqlServerOptions.CompatibilityLevelWithoutDefault;
+ }
+ }
+
+ ///
+ /// 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 void Validate(IDbContextOptions options)
+ {
+ var sqlserverOptions = options.FindExtension();
+
+ if (sqlserverOptions != null &&
+ (CompatibilityLevelWithoutDefault != sqlserverOptions.CompatibilityLevelWithoutDefault
+ || CompatibilityLevel != sqlserverOptions.CompatibilityLevel))
+ {
+ throw new InvalidOperationException(
+ CoreStrings.SingletonOptionChanged(
+ nameof(SqlServerDbContextOptionsExtensions.UseSqlServer),
+ nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs b/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs
index 25c9312e95a..066f6eccc03 100644
--- a/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs
+++ b/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs
@@ -103,4 +103,17 @@ public virtual SqlServerDbContextOptionsBuilder EnableRetryOnFailure(
TimeSpan maxRetryDelay,
IEnumerable? errorNumbersToAdd)
=> ExecutionStrategy(c => new SqlServerRetryingExecutionStrategy(c, maxRetryCount, maxRetryDelay, errorNumbersToAdd));
+
+ ///
+ /// Sets the SQL Server compatibility level that EF Core will use when interacting with the database. This allows configuring EF
+ /// Core to work with older (or newer) versions of SQL Server. Defaults to 160 (SQL Server 2022).
+ ///
+ ///
+ /// See Using DbContextOptions, and
+ /// SQL Server
+ /// documentation on compatibility level for more information and examples.
+ ///
+ /// to have null resource
+ public virtual SqlServerDbContextOptionsBuilder UseCompatibilityLevel(int compatibilityLevel)
+ => WithOption(e => e.WithCompatibilityLevel(compatibilityLevel));
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
index 3e0407691a0..7ea65b2626a 100644
--- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
@@ -670,6 +670,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
@@ -765,4 +786,25 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
///
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
+ /// 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)
+ {
+ 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/SqlServerOpenJsonExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
new file mode 100644
index 00000000000..c46b07f60ae
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
@@ -0,0 +1,149 @@
+// 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(
+ 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;
+ }
+
+ ///
+ /// 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 Update(
+ SqlExpression jsonExpression,
+ SqlExpression? path,
+ IReadOnlyList? columnInfos = null)
+ => jsonExpression == JsonExpression
+ && path == Path
+ && (columnInfos is null ? ColumnInfos is null : ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos))
+ ? this
+ : new SqlServerOpenJsonExpression(Alias, jsonExpression, path, 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)
+ => 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/SqlServerQueryRootProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs
new file mode 100644
index 00000000000..eaff1e7eb80
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs
@@ -0,0 +1,44 @@
+// 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.SqlServer.Infrastructure.Internal;
+
+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 SqlServerQueryRootProcessor : RelationalQueryRootProcessor
+{
+ 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
+ /// 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 SqlServerQueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ => _supportsOpenJson = sqlServerSingletonOptions.CompatibilityLevel >= 130;
+
+ ///
+ /// Indicates that a can be converted to a , if the
+ /// configured SQL Server version supports OPENJSON.
+ ///
+ ///
+ /// 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 ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => _supportsOpenJson;
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index 3f7fd0bc9eb..227a5329192 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 AS x 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,62 @@ 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)
+ {
+ // SQL Server supports providing the names of columns projected out of VALUES: (VALUES (1, 3), (2, 4)) AS x(a, b)
+ // (this is implemented in VisitValues above).
+ // 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++)
+ {
+ 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 +376,9 @@ when tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationTy
case SqlServerAggregateFunctionExpression aggregateFunctionExpression:
return VisitSqlServerAggregateFunction(aggregateFunctionExpression);
+
+ case SqlServerOpenJsonExpression openJsonExpression:
+ return VisitOpenJsonExpression(openJsonExpression);
}
return base.VisitExtension(extensionExpression);
@@ -381,6 +455,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..f22ddc0236a
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs
@@ -0,0 +1,41 @@
+// 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.SqlServer.Infrastructure.Internal;
+
+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
+{
+ private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;
+
+ ///
+ /// 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 SqlServerQueryTranslationPreprocessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ => _sqlServerSingletonOptions = sqlServerSingletonOptions;
+
+ ///
+ /// 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 ProcessQueryRoots(Expression expression)
+ => new SqlServerQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext, _sqlServerSingletonOptions)
+ .Visit(expression);
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs
new file mode 100644
index 00000000000..ca3f6d16d68
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs
@@ -0,0 +1,53 @@
+// 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.SqlServer.Infrastructure.Internal;
+
+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 SqlServerQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
+{
+ private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;
+
+ ///
+ /// 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 SqlServerQueryTranslationPreprocessorFactory(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
+ {
+ Dependencies = dependencies;
+ RelationalDependencies = relationalDependencies;
+ _sqlServerSingletonOptions = sqlServerSingletonOptions;
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
+ => new SqlServerQueryTranslationPreprocessor(
+ Dependencies, RelationalDependencies, _sqlServerSingletonOptions, queryCompilationContext);
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index ea225751457..604c95e619a 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;
}
///
@@ -100,6 +111,86 @@ protected override Expression VisitExtension(Expression extensionExpression)
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 TranslateCollection(
+ SqlExpression sqlExpression,
+ RelationalTypeMapping? elementTypeMapping,
+ string tableAlias)
+ {
+ var elementClrType = sqlExpression.Type.GetSequenceType();
+
+ // Generate the OpenJson function expression, and wrap it in a SelectExpression.
+ // Note that we want to preserve the ordering of the element's, i.e. for the rows coming out of OpenJson to be the same as the
+ // element order in the original JSON array.
+ // Unfortunately, OpenJson with an explicit schema (with the WITH clause) doesn't support this; so we use the variant with the
+ // default schema, which returns a 'key' column containing the index, and order by that. This also means we need to explicitly
+ // apply a conversion from the values coming out of OpenJson (always NVARCHAR(MAX)) to the required relational store type.
+ var openJsonExpression = new TableValuedFunctionExpression(tableAlias, "OpenJson", new[] { sqlExpression });
+
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression
+ var selectExpression = new SelectExpression(openJsonExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, isColumnNullable: null);
+
+ if (elementTypeMapping is { StoreType: not "nvarchar(max)" })
+ {
+ // For columns (where we know the type mapping), we need to overwrite the projection in order to insert a CAST() to the actual
+ // relational store type we expect out of the JSON array (e.g. OpenJson returns strings, we want datetime2).
+ // For parameters (where we don't yet know the type mapping), we'll need to do that later, after the type mapping has been
+ // inferred.
+ // TODO: Need to pass through the type mapping API for converting the JSON value (nvarchar) to the relational store type (e.g.
+ // datetime2), see #30677
+ selectExpression.ReplaceProjection(
+ new Dictionary
+ {
+ {
+ new ProjectionMember(), _sqlExpressionFactory.Convert(
+ selectExpression.CreateColumnExpression(
+ openJsonExpression,
+ "value",
+ typeof(string),
+ _typeMappingSource.FindMapping("nvarchar(max)"),
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to
+ // SelectExpression
+ columnNullable: null),
+ elementClrType,
+ elementTypeMapping)
+ }
+ });
+ }
+
+ // Append an ordering for the OpenJson 'key' column, converting it from nvarchar to int.
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ _sqlExpressionFactory.Convert(
+ selectExpression.CreateColumnExpression(
+ openJsonExpression,
+ "key",
+ typeof(string),
+ typeMapping: _typeMappingSource.FindMapping("nvarchar(8000)"),
+ columnNullable: false),
+ typeof(int),
+ _typeMappingSource.FindMapping(typeof(int))),
+ ascending: true));
+
+ 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);
+ }
+
///
/// 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,148 @@ 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 override Expression ApplyInferredTypeMappings(
+ Expression expression,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => new SqlServerInferredTypeMappingApplier(_typeMappingSource, _sqlExpressionFactory, 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
+ /// 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 SqlServerInferredTypeMappingApplier : RelationalInferredTypeMappingApplier
+ {
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+ private Dictionary? _currentSelectInferredTypeMappings;
+
+ ///
+ /// 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 SqlServerInferredTypeMappingApplier(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ : base(inferredTypeMappings)
+ => (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, 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
+ /// 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)
+ {
+ switch (expression)
+ {
+ case TableValuedFunctionExpression { Name: "OpenJson", Schema: null, IsBuiltIn: true } openJsonExpression
+ when InferredTypeMappings.TryGetValue((openJsonExpression, "value"), out var typeMapping):
+ return ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping });
+
+ // Above, we applied the type mapping the the parameter that OpenJson accepts as an argument.
+ // But the inferred type mapping also needs to be applied as a SQL conversion on the column projections coming out of the
+ // SelectExpression containing the OpenJson call. So we set state to know about OpenJson tables and their type mappings
+ // in the immediate SelectExpression, and continue visiting down (see ColumnExpression visitation below).
+ case SelectExpression selectExpression:
+ {
+ Dictionary? previousSelectInferredTypeMappings = null;
+
+ foreach (var table in selectExpression.Tables)
+ {
+ if (table is TableValuedFunctionExpression { Name: "OpenJson", Schema: null, IsBuiltIn: true } openJsonExpression
+ && InferredTypeMappings.TryGetValue((openJsonExpression, "value"), out var inferredTypeMapping))
+ {
+ if (previousSelectInferredTypeMappings is null)
+ {
+ previousSelectInferredTypeMappings = _currentSelectInferredTypeMappings;
+ _currentSelectInferredTypeMappings = new();
+ }
+
+ _currentSelectInferredTypeMappings![openJsonExpression] = inferredTypeMapping;
+ }
+ }
+
+ var visited = base.VisitExtension(expression);
+
+ _currentSelectInferredTypeMappings = previousSelectInferredTypeMappings;
+
+ return visited;
+ }
+
+ case ColumnExpression { Name: "value" } columnExpression
+ when _currentSelectInferredTypeMappings is not null
+ && _currentSelectInferredTypeMappings.TryGetValue(columnExpression.Table, out var inferredTypeMapping):
+ return ApplyTypeMappingOnColumn(columnExpression, inferredTypeMapping);
+
+ default:
+ return 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.
+ ///
+ protected virtual TableValuedFunctionExpression ApplyTypeMappingsOnOpenJsonExpression(
+ TableValuedFunctionExpression openJsonExpression,
+ IReadOnlyList typeMappings)
+ {
+ Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1");
+ var elementTypeMapping = typeMappings[0];
+
+ // Constant queryables are translated to VALUES, no need for JSON.
+ // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them.
+ if (openJsonExpression.Arguments[0] is not SqlParameterExpression parameterExpression)
+ {
+ return openJsonExpression;
+ }
+
+ // TODO: We shouldn't need to manually construct the JSON string type mapping this way; we need to be able to provide the
+ // TODO: element's store type mapping as input to _typeMappingSource.FindMapping.
+ // TODO: When this is done, revert converter equality check in QuerySqlGenerator.VisitSqlParameter back to reference equality,
+ // since we'll always have the same instance of the type mapping returned from the type mapping source. Also remove
+ // CollectionToJsonStringConverter.Equals etc.
+ // TODO: Note: NpgsqlTypeMappingSource exposes FindContainerMapping() for this purpose.
+ if (_typeMappingSource.FindMapping(typeof(string)) is not SqlServerStringTypeMapping parameterTypeMapping)
+ {
+ throw new InvalidOperationException("Type mapping for 'string' could not be found or was not a SqlServerStringTypeMapping");
+ }
+
+ parameterTypeMapping = (SqlServerStringTypeMapping)parameterTypeMapping
+ .Clone(new CollectionToJsonStringConverter(parameterExpression.Type, elementTypeMapping));
+
+ parameterTypeMapping = (SqlServerStringTypeMapping)parameterTypeMapping.CloneWithElementTypeMapping(elementTypeMapping);
+
+ var arguments = openJsonExpression.Arguments.ToArray();
+ arguments[0] = parameterExpression.ApplyTypeMapping(parameterTypeMapping);
+ return openJsonExpression.Update(arguments);
+ }
+
+ ///
+ /// 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 ApplyTypeMappingOnColumn(ColumnExpression columnExpression, RelationalTypeMapping typeMapping)
+ // OpenJson's value column has type nvarchar(max); apply a CAST() unless that's the inferred element type mapping
+ => typeMapping.StoreType is "nvarchar(max)"
+ ? columnExpression
+ : _sqlExpressionFactory.Convert(columnExpression, typeMapping.ClrType, typeMapping);
+ }
}
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
index 8d0d053cd62..751da51b988 100644
--- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
+++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
@@ -119,6 +119,17 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
return new SqlServerStringTypeMapping(parameters, _sqlDbType);
}
+ ///
+ /// 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 SqlServerStringTypeMapping(
+ Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)),
+ _sqlDbType);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
index 21f5884a764..2394c2e3892 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)
{
@@ -415,6 +423,87 @@ 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 IEnumerable.
+ // 2. The store type is either not given or a string type.
+ // 3. The element CLR type has a supported type mapping which isn't itself a collection (nested collections not yet supported).
+
+ // Note that e.g. Newtonsoft.Json's JToken is enumerable over itself, exclude that scenario to avoid stack overflow.
+ if (mappingInfo.ClrType?.TryGetElementType(typeof(IEnumerable<>)) is not { } elementClrType
+ || elementClrType == mappingInfo.ClrType)
+ {
+ 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
+
+ // Make sure the element type is mapped and isn't itself a collection (nested collections not supported)
+ if (FindMapping(elementClrType) is not { ElementTypeMapping: null } 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)
+ {
+ // The JSON representation for new[] { 1, 2 } is AQI= (base64?), this cannot simply be cast to varbinary(max) (0x0102)
+ if (elementTypeMapping is not SqlServerByteArrayTypeMapping)
+ {
+ stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping);
+ }
+ }
+
+ return stringTypeMapping;
+ }
+
private static readonly List NameBasesUsingPrecision = new()
{
"decimal",
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
index ed862f528a2..f88ec4741b3 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
@@ -110,6 +110,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs
new file mode 100644
index 00000000000..15abafb0867
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Data.Sqlite;
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.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 SqliteQueryRootProcessor : RelationalQueryRootProcessor
+{
+ 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
+ /// 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 SqliteQueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ => _areJsonFunctionsSupported = new Version(new SqliteConnection().ServerVersion) >= new Version(3, 38);
+
+
+ ///
+ /// Indicates that a can be converted to a , if the
+ /// configured SQL Server version supports JSON functions (json_each).
+ ///
+ ///
+ /// 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 ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => _areJsonFunctionsSupported;
+}
diff --git a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs
similarity index 57%
rename from src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs
rename to src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs
index 7a433e8c5c5..214787bd6f2 100644
--- a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.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.Sqlite.Query.Internal;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -9,19 +9,20 @@ 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 SqliteQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
{
- private readonly IModel _model;
-
///
/// 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 TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model)
+ public SqliteQueryTranslationPreprocessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
{
- _model = model;
}
///
@@ -30,18 +31,7 @@ public TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model)
/// 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);
+ protected override Expression ProcessQueryRoots(Expression expression)
+ => new SqliteQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext)
+ .Visit(expression);
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs
new file mode 100644
index 00000000000..e7a8c021558
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs
@@ -0,0 +1,46 @@
+// 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.Sqlite.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 SqliteQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
+{
+ ///
+ /// 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 SqliteQueryTranslationPreprocessorFactory(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
+ {
+ Dependencies = dependencies;
+ RelationalDependencies = relationalDependencies;
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
+ => new SqliteQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext);
+}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
index 140244153c2..d0728006d3e 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,8 +120,238 @@ 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
+ {
+ Tables: [TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true, 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.UpdateQueryExpression(_sqlExpressionFactory.Select(translation));
+ }
+
+ 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 TranslateCollection(
+ SqlExpression sqlExpression,
+ RelationalTypeMapping? elementTypeMapping,
+ string tableAlias)
+ {
+ var elementClrType = sqlExpression.Type.GetSequenceType();
+
+ var jsonEachExpression = new TableValuedFunctionExpression(tableAlias, "json_each", new[] { sqlExpression });
+
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression
+ var selectExpression = new SelectExpression(
+ jsonEachExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping,
+ isColumnNullable: null);
+
+ // TODO: SQLite does have REAL and BLOB types, which JSON does not. Need to possibly cast to that.
+ if (elementTypeMapping is not null)
+ {
+ // TODO: In any case, we still ned to pass through the type mapping API for doing any conversions (e.g. for datetime, from JSON
+ // ISO8601 to SQLite's format without the T), see #30677. Do this here.
+ }
+
+ // Append an ordering for the json_each 'key' column.
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ jsonEachExpression,
+ "key",
+ typeof(int),
+ typeMapping: _typeMappingSource.FindMapping(typeof(int)),
+ columnNullable: false),
+ ascending: true));
+
+ 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
?? expression.Type;
+
+ ///
+ /// 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 ApplyInferredTypeMappings(
+ Expression expression,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => new SqliteInferredTypeMappingApplier(_typeMappingSource, _sqlExpressionFactory, 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
+ /// 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 SqliteInferredTypeMappingApplier : RelationalInferredTypeMappingApplier
+ {
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+ private Dictionary? _currentSelectInferredTypeMappings;
+
+ ///
+ /// 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 SqliteInferredTypeMappingApplier(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ : base(inferredTypeMappings)
+ => (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, 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
+ /// 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)
+ {
+ switch (expression)
+ {
+ case TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true } jsonEachExpression
+ when InferredTypeMappings.TryGetValue((jsonEachExpression, "value"), out var typeMapping):
+ return ApplyTypeMappingsOnJsonEachExpression(jsonEachExpression, typeMapping);
+
+ // Above, we applied the type mapping the the parameter that json_each accepts as an argument.
+ // But the inferred type mapping also needs to be applied as a SQL conversion on the column projections coming out of the
+ // SelectExpression containing the json_each call. So we set state to know about json_each tables and their type mappings
+ // in the immediate SelectExpression, and continue visiting down (see ColumnExpression visitation below).
+ case SelectExpression selectExpression:
+ {
+ Dictionary? previousSelectInferredTypeMappings = null;
+
+ foreach (var table in selectExpression.Tables)
+ {
+ if (table is TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true } jsonEachExpression
+ && InferredTypeMappings.TryGetValue((jsonEachExpression, "value"), out var inferredTypeMapping))
+ {
+ if (previousSelectInferredTypeMappings is null)
+ {
+ previousSelectInferredTypeMappings = _currentSelectInferredTypeMappings;
+ _currentSelectInferredTypeMappings = new();
+ }
+
+ _currentSelectInferredTypeMappings![jsonEachExpression] = inferredTypeMapping;
+ }
+ }
+
+ var visited = base.VisitExtension(expression);
+
+ _currentSelectInferredTypeMappings = previousSelectInferredTypeMappings;
+
+ return visited;
+ }
+
+ case ColumnExpression { Name: "value" } columnExpression
+ when _currentSelectInferredTypeMappings is not null
+ && _currentSelectInferredTypeMappings.TryGetValue(columnExpression.Table, out var inferredTypeMapping):
+ return ApplyTypeMappingOnColumn(columnExpression, inferredTypeMapping);
+
+ default:
+ return 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.
+ ///
+ protected virtual TableValuedFunctionExpression ApplyTypeMappingsOnJsonEachExpression(
+ TableValuedFunctionExpression jsonEachExpression,
+ RelationalTypeMapping inferredTypeMapping)
+ {
+ // Constant queryables are translated to VALUES, no need for JSON.
+ // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them.
+ if (jsonEachExpression.Arguments[0] is not SqlParameterExpression parameterExpression)
+ {
+ return jsonEachExpression;
+ }
+
+ // TODO: We shouldn't need to manually construct the JSON string type mapping this way; we need to be able to provide the
+ // TODO: element's store type mapping as input to _typeMappingSource.FindMapping.
+ if (_typeMappingSource.FindMapping(typeof(string)) is not SqliteStringTypeMapping parameterTypeMapping)
+ {
+ throw new InvalidOperationException("Type mapping for 'string' could not be found or was not a SqliteStringTypeMapping");
+ }
+
+ parameterTypeMapping = (SqliteStringTypeMapping)parameterTypeMapping
+ .Clone(new CollectionToJsonStringConverter(parameterExpression.Type, inferredTypeMapping));
+
+ parameterTypeMapping = (SqliteStringTypeMapping)parameterTypeMapping.CloneWithElementTypeMapping(inferredTypeMapping);
+
+ return jsonEachExpression.Update(new[] { parameterExpression.ApplyTypeMapping(parameterTypeMapping) });
+ }
+
+ ///
+ /// 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 ApplyTypeMappingOnColumn(ColumnExpression columnExpression, RelationalTypeMapping typeMapping)
+ => typeMapping switch
+ {
+ // TODO: These server-side conversions need to be managed on the type mapping
+
+ // The "standard" JSON timestamp representation is ISO8601, with a T between date and time; but SQLite's representation has
+ // no T. Apply a conversion on the value coming out of json_each.
+ SqliteDateTimeTypeMapping => _sqlExpressionFactory.Function(
+ "datetime", new[] { columnExpression }, nullable: true, new[] { true }, typeof(DateTime), typeMapping),
+
+ SqliteGuidTypeMapping => _sqlExpressionFactory.Function(
+ "upper", new[] { columnExpression }, nullable: true, new[] { true }, typeof(Guid), typeMapping),
+
+ _ => columnExpression
+ };
+ }
}
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..08b5cdc4b89 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,69 @@ public static bool IsSpatialiteType(string columnType)
return null;
}
+ private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo)
+ {
+ // Make sure the element type is mapped and isn't itself a collection (nested collections not supported)
+ if (mappingInfo is { StoreTypeName: TextTypeName or null }
+ && mappingInfo.ClrType?.TryGetElementType(typeof(IEnumerable<>)) is { } elementClrType
+ && FindMapping(elementClrType) is { ElementTypeMapping: null } 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)
+ {
+ switch (elementTypeMapping)
+ {
+ // The JSON representation for DateTimeOffset is ISO8601 (2023-01-01T12:30:00+02:00), but our SQL literal representation
+ // is 2023-01-01 12:30:00+02:00 (no T).
+ // datetime('2023-01-01T12:30:00+02:00') yields '2023-01-01 10:30:00' - converted to UTC, no timezone.
+ case SqliteDateTimeOffsetTypeMapping:
+ // The JSON representation for decimal is e.g. 1 (JSON int), whereas our literal representation is "1.0" (string)
+ case SqliteDecimalTypeMapping:
+ // The JSON representation for new[] { 1, 2 } is AQI= (base64?), our SQL literal representation is X'0102'
+ case ByteArrayTypeMapping:
+ break;
+
+
+ default:
+ stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping);
+ break;
+ }
+ }
+
+ return stringTypeMapping;
+ }
+
+ return null;
+ }
+
private readonly Func[] _typeRules =
{
name => Contains(name, "INT")
diff --git a/src/EFCore/Query/InlineQueryRootExpression.cs b/src/EFCore/Query/InlineQueryRootExpression.cs
new file mode 100644
index 00000000000..3106f618c8a
--- /dev/null
+++ b/src/EFCore/Query/InlineQueryRootExpression.cs
@@ -0,0 +1,93 @@
+// 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 an inline query root within the query (e.g. new[] { 1, 2, 3 }).
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally not used in application code.
+///
+///
+public class InlineQueryRootExpression : QueryRootExpression
+{
+ ///
+ /// The values contained in this query root.
+ ///
+ public virtual IReadOnlyList Values { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The query provider associated with this query root.
+ /// The values contained in this query root.
+ /// The element type this query root represents.
+ public InlineQueryRootExpression(IAsyncQueryProvider asyncQueryProvider, IReadOnlyList values, Type elementType)
+ : base(asyncQueryProvider, elementType)
+ {
+ Values = values;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// An expression containing the values that this query root represents.
+ /// The element type this query root represents.
+ public InlineQueryRootExpression(IReadOnlyList values, Type elementType)
+ : base(elementType)
+ {
+ Values = values;
+ }
+
+ ///
+ public override Expression DetachQueryProvider()
+ => new InlineQueryRootExpression(Values, ElementType);
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ Expression[]? newValues = null;
+
+ for (var i = 0; i < Values.Count; i++)
+ {
+ var value = Values[i];
+ var newValue = visitor.Visit(value);
+
+ if (newValue != value && newValues is null)
+ {
+ newValues = new Expression[Values.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newValues[j] = Values[j];
+ }
+ }
+
+ if (newValues is not null)
+ {
+ newValues[i] = newValue;
+ }
+ }
+
+ return newValues is null ? this : new InlineQueryRootExpression(newValues, Type);
+ }
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("[");
+
+ for (var i = 0; i < Values.Count; i++)
+ {
+ if (i > 0)
+ {
+ expressionPrinter.Append(",");
+ }
+
+ expressionPrinter.Visit(Values[i]);
+ }
+
+ expressionPrinter.Append("]");
+ }
+}
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs
index a09f19db5d3..82c42039939 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 PrimitiveCollectionReference(root, property);
}
}
@@ -1015,6 +1027,9 @@ private sealed class ReducingExpressionVisitor : ExpressionVisitor
case OwnedNavigationReference ownedNavigationReference:
return Visit(ownedNavigationReference.Parent).CreateEFPropertyExpression(ownedNavigationReference.Navigation);
+ case PrimitiveCollectionReference 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..6e2ef8d64c6 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
@@ -501,4 +501,44 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
}
}
}
+
+ ///
+ /// Queryable properties are not expanded (similar to .
+ ///
+ private sealed class PrimitiveCollectionReference : Expression, IPrintableExpression
+ {
+ public PrimitiveCollectionReference(Expression parent, IProperty property)
+ {
+ Parent = parent;
+ Property = property;
+ }
+
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ Parent = visitor.Visit(Parent);
+
+ return this;
+ }
+
+ public Expression Parent { get; private set; }
+ public new IProperty Property { 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..923b9da71ce 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
@@ -1954,17 +1954,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);
@@ -2048,14 +2037,37 @@ 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 PrimitiveCollectionReference primitiveCollectionReference:
+ {
+ var currentTree = new NavigationTreeExpression(Expression.Default(primitiveCollectionReference.Type.GetSequenceType()));
+
+ return new NavigationExpansionExpression(
+ Expression.Call(
+ QueryableMethods.AsQueryable.MakeGenericMethod(primitiveCollectionReference.Type.GetSequenceType()),
+ primitiveCollectionReference),
+ currentTree,
+ currentTree,
+ GetParameterName("p"));
+ }
+
+ default:
+ return expression;
+ }
}
private string GetParameterName(string prefix)
diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
index b8ae88c3639..2f5dec852bc 100644
--- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
@@ -252,9 +252,8 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa
}
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);
}
@@ -386,9 +385,8 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa
private Expression TryConvertListContainsToQueryableContains(MethodCallExpression methodCallExpression)
{
- if (ClientSource(methodCallExpression.Object))
+ if (methodCallExpression.Object is MemberInitExpression or NewExpression)
{
- // this is methodCall over closure variable or constant
return base.VisitMethodCall(methodCallExpression);
}
@@ -402,13 +400,6 @@ 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 CanConvertEnumerableToQueryable(Type enumerableType, Type queryableType)
{
if (enumerableType == typeof(IEnumerable)
diff --git a/src/EFCore/Query/ParameterQueryRootExpression.cs b/src/EFCore/Query/ParameterQueryRootExpression.cs
new file mode 100644
index 00000000000..81d9e44f966
--- /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 virtual 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..ac98c75b4f9
--- /dev/null
+++ b/src/EFCore/Query/QueryRootProcessor.cs
@@ -0,0 +1,135 @@
+// 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 Microsoft.EntityFrameworkCore.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+///
+/// A visitor which adds additional query root nodes during preprocessing.
+///
+public class QueryRootProcessor : ExpressionVisitor
+{
+ private readonly IModel _model;
+
+ ///
+ /// Creates a new instance of the class with associated query provider.
+ ///
+ /// Parameter object containing dependencies for this class.
+ /// The query compilation context object to use.
+ public QueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ QueryCompilationContext queryCompilationContext)
+ {
+ _model = queryCompilationContext.Model;
+ }
+
+ ///
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ // We'll look for IEnumerable/IQueryable arguments to methods on Enumerable/Queryable, and convert these to constant/parameter query
+ // root nodes. These will later get translated to e.g. VALUES (constant) and OPENJSON (parameter) on SQL Server.
+ var method = methodCallExpression.Method;
+ if (method.DeclaringType != typeof(Queryable)
+ && method.DeclaringType != typeof(Enumerable)
+ && method.DeclaringType != typeof(QueryableExtensions)
+ && method.DeclaringType != typeof(EntityFrameworkQueryableExtensions))
+ {
+ return base.VisitMethodCall(methodCallExpression);
+ }
+
+ 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;
+
+ // This converts collections over constants and parameters to query roots, for later translation of LINQ operators over them.
+ // The element type doesn't have to be directly mappable; we allow unknown CLR types in order to support value convertors
+ // (the precise type mapping - with the value converter - will be inferred later based on LINQ operators composed on the root).
+ // However, we do exclude element CLR types which are associated to entity types in our model, since Contains over entity
+ // collections isn't yet supported (#30712).
+ if (parameterType.IsGenericType
+ && (parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
+ || parameterType.GetGenericTypeDefinition() == typeof(IQueryable<>))
+ && parameterType.GetGenericArguments()[0] is var elementClrType
+ && !_model.FindEntityTypes(elementClrType).Any())
+ {
+ switch (argument)
+ {
+ case ConstantExpression { Value: IEnumerable values } constantExpression
+ when ShouldConvertToInlineQueryRoot(constantExpression):
+
+ var valueExpressions = new List();
+ foreach (var value in values)
+ {
+ valueExpressions.Add(Expression.Constant(value, elementClrType));
+ }
+ visitedArgument = new InlineQueryRootExpression(valueExpressions, elementClrType);
+ break;
+
+ // TODO: Support NewArrayExpression, see #30734.
+
+ case ParameterExpression parameterExpression
+ when parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)
+ == true
+ && ShouldConvertToParameterQueryRoot(parameterExpression):
+ visitedArgument = new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression);
+ break;
+
+ default:
+ visitedArgument = null;
+ break;
+ }
+ }
+
+ 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);
+ }
+
+ ///
+ /// Determines whether a should be converted to a .
+ /// This handles cases inline expressions whose elements are all constants.
+ ///
+ /// The constant expression that's a candidate for conversion to a query root.
+ protected virtual bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression)
+ => false;
+
+ ///
+ /// Determines whether a should be converted to a .
+ ///
+ /// The parameter expression that's a candidate for conversion to a query root.
+ protected virtual bool ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => false;
+}
+
diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs
index 1f2a0cc307b..5fd863e6581 100644
--- a/src/EFCore/Query/QueryTranslationPreprocessor.cs
+++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs
@@ -77,6 +77,18 @@ 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);
+ expression = ProcessQueryRoots(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.
+ protected virtual Expression ProcessQueryRoots(Expression expression)
+ => expression;
}
diff --git a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs
index f308fd98fe6..53abf26c65f 100644
--- a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs
+++ b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs
@@ -46,13 +46,20 @@ public sealed record QueryTranslationPreprocessorDependencies
///
[EntityFrameworkInternal]
public QueryTranslationPreprocessorDependencies(
+ ITypeMappingSource typeMappingSource,
IEvaluatableExpressionFilter evaluatableExpressionFilter,
INavigationExpansionExtensibilityHelper navigationExpansionExtensibilityHelper)
{
+ TypeMappingSource = typeMappingSource;
EvaluatableExpressionFilter = evaluatableExpressionFilter;
NavigationExpansionExtensibilityHelper = navigationExpansionExtensibilityHelper;
}
+ ///
+ /// Type mapping source.
+ ///
+ public ITypeMappingSource TypeMappingSource { get; init; }
+
///
/// Evaluatable expression filter.
///
diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
index 4b90c04448f..7d4ab4a455e 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,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
}
throw new InvalidOperationException(
- CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName()));
+ TranslationErrorDetails is null
+ ? CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName())
+ : CoreStrings.TranslationFailedWithDetails(queryRootExpression, TranslationErrorDetails));
}
return base.VisitExtension(extensionExpression);
@@ -508,7 +518,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..868cb139aea 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..90d85bb48cb
--- /dev/null
+++ b/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs
@@ -0,0 +1,64 @@
+// 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: Value converters on the element type mapping should be supported
+ // TODO: Full sanitization/nullability
+ ConvertToProvider = x => x is null ? "[]" : JsonSerializer.Serialize(x);
+ ConvertFromProvider = o
+ => o is string s
+ ? JsonSerializer.Deserialize(s, modelClrType)!
+ : throw new ArgumentException(); // TODO
+ }
+
+ ///
+ public override Func ConvertToProvider { get; }
+
+ ///
+ public override Func ConvertFromProvider { get; }
+
+ ///
+ public override Type ModelClrType { get; }
+
+ ///
+ public override Type ProviderClrType
+ => typeof(string);
+
+ ///
+ public override bool Equals(object? obj)
+ => ReferenceEquals(this, obj) || (obj is CollectionToJsonStringConverter other && Equals(other));
+
+ private bool Equals(CollectionToJsonStringConverter other)
+ => ModelClrType == other.ModelClrType && _elementTypeMapping.Equals(other._elementTypeMapping);
+
+ ///
+ public override int GetHashCode()
+ => ModelClrType.GetHashCode();
+}
diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
index 3cabc98b4f3..d30276b6423 100644
--- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
+++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
@@ -23,20 +23,6 @@ public override void Properties_can_be_made_concurrency_tokens()
Assert.Throws(
() => base.Properties_can_be_made_concurrency_tokens()).Message);
- protected override void Mapping_throws_for_non_ignored_array()
- {
- var modelBuilder = CreateModelBuilder();
-
- modelBuilder.Entity();
-
- var model = modelBuilder.FinalizeModel();
- var entityType = model.FindEntityType(typeof(OneDee));
-
- var property = entityType.FindProperty(nameof(OneDee.One));
- Assert.Null(property.GetProviderClrType());
- Assert.NotNull(property.FindTypeMapping());
- }
-
public override void Properties_can_have_provider_type_set_for_type()
{
var modelBuilder = CreateModelBuilder(c => c.Properties().HaveConversion());
diff --git a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs
index 3d82ee354f5..24ddf62a94b 100644
--- a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs
+++ b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs
@@ -3,6 +3,7 @@
using System.Numerics;
using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using Microsoft.EntityFrameworkCore.TestUtilities.Xunit;
@@ -826,8 +827,8 @@ private static SqlServerTypeMappingSource CreateTypeMappingSource(
params IRelationalTypeMappingSourcePlugin[] plugins)
=> new(
TestServiceFactory.Instance.Create(),
- new RelationalTypeMappingSourceDependencies(
- plugins));
+ new RelationalTypeMappingSourceDependencies(plugins),
+ new SqlServerSingletonOptions());
private class TestTypeMappingPlugin : IRelationalTypeMappingSourcePlugin
{
diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs
index a7e5224d1dd..ff59d0d87d8 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using NetTopologySuite;
using NetTopologySuite.Geometries;
@@ -19,7 +20,8 @@ public void Generate_separates_operations_by_a_blank_line()
new CSharpHelper(
new SqlServerTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create()))));
+ TestServiceFactory.Instance.Create(),
+ new SqlServerSingletonOptions()))));
var builder = new IndentedStringBuilder();
@@ -3158,7 +3160,8 @@ private void Test(T operation, string expectedCode, Action assert)
new IRelationalTypeMappingSourcePlugin[]
{
new SqlServerNetTopologySuiteTypeMappingSourcePlugin(NtsGeometryServices.Instance)
- })))));
+ }),
+ new SqlServerSingletonOptions()))));
var builder = new IndentedStringBuilder();
generator.Generate("mb", new[] { operation }, builder);
diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs
index b15e1b8dcc2..5c5d872aefb 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs
@@ -6,6 +6,7 @@
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
// ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local
@@ -346,7 +347,8 @@ private static void MissingAnnotationCheck(
{
var sqlServerTypeMappingSource = new SqlServerTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create());
+ TestServiceFactory.Instance.Create(),
+ new SqlServerSingletonOptions());
var sqlServerAnnotationCodeGenerator = new SqlServerAnnotationCodeGenerator(
new AnnotationCodeGeneratorDependencies(sqlServerTypeMappingSource));
@@ -448,7 +450,8 @@ public void Snapshot_with_enum_discriminator_uses_converted_values()
{
var sqlServerTypeMappingSource = new SqlServerTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create());
+ TestServiceFactory.Instance.Create(),
+ new SqlServerSingletonOptions());
var codeHelper = new CSharpHelper(
sqlServerTypeMappingSource);
@@ -505,7 +508,8 @@ private static void AssertConverter(ValueConverter valueConverter, string expect
var sqlServerTypeMappingSource = new SqlServerTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create());
+ TestServiceFactory.Instance.Create(),
+ new SqlServerSingletonOptions());
var codeHelper = new CSharpHelper(sqlServerTypeMappingSource);
diff --git a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs
index 2c4d6144fa7..efc9abacced 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs
@@ -6,6 +6,7 @@
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using Microsoft.EntityFrameworkCore.TestUtilities.FakeProvider;
using Microsoft.EntityFrameworkCore.Update.Internal;
@@ -57,7 +58,8 @@ private IMigrationsScaffolder CreateMigrationScaffolder()
var idGenerator = new MigrationsIdGenerator();
var sqlServerTypeMappingSource = new SqlServerTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create());
+ TestServiceFactory.Instance.Create(),
+ new SqlServerSingletonOptions());
var sqlServerAnnotationCodeGenerator = new SqlServerAnnotationCodeGenerator(
new AnnotationCodeGeneratorDependencies(sqlServerTypeMappingSource));
var code = new CSharpHelper(sqlServerTypeMappingSource);
diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
index 86fec919a96..82f6d78427c 100644
--- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
@@ -7,6 +7,7 @@
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using NetTopologySuite;
@@ -7585,7 +7586,8 @@ protected CSharpMigrationsGenerator CreateMigrationsGenerator()
new IRelationalTypeMappingSourcePlugin[]
{
new SqlServerNetTopologySuiteTypeMappingSourcePlugin(NtsGeometryServices.Instance)
- }));
+ }),
+ new SqlServerSingletonOptions());
var codeHelper = new CSharpHelper(sqlServerTypeMappingSource);
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ScaffoldingTypeMapperSqlServerTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ScaffoldingTypeMapperSqlServerTest.cs
index 57fe5a87e2a..0c1f0d4311e 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/ScaffoldingTypeMapperSqlServerTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ScaffoldingTypeMapperSqlServerTest.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
// ReSharper disable CheckNamespace
@@ -403,5 +404,6 @@ private static ScaffoldingTypeMapper CreateMapper()
=> new(
new SqlServerTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create()));
+ TestServiceFactory.Instance.Create(),
+ new SqlServerSingletonOptions()));
}
diff --git a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs
index f0dc6497a5b..5708ab1eefc 100644
--- a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs
@@ -8,6 +8,8 @@ public class InMemoryComplianceTest : ComplianceTestBase
protected override ICollection IgnoredTestBases { get; } = new HashSet
{
// No in-memory tests
+ typeof(PrimitiveCollectionsQueryTestBase<>),
+ typeof(NonSharedPrimitiveCollectionsQueryTestBase),
typeof(FunkyDataQueryTestBase<>),
typeof(StoreGeneratedTestBase<>),
typeof(ConferencePlannerTestBase<>),
diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs
index 8902ca5c98d..25a49c3c5cb 100644
--- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.TestModels.Northwind;
+
namespace Microsoft.EntityFrameworkCore.Query;
public class NorthwindJoinQueryInMemoryTest : NorthwindJoinQueryTestBase>
@@ -30,4 +32,24 @@ public override Task SelectMany_with_client_eval_with_collection_shaper_ignored(
public override Task SelectMany_with_client_eval_with_constructor(bool async)
// Joins between sources with client eval. Issue #21200.
=> Assert.ThrowsAsync(() => base.SelectMany_with_client_eval_with_constructor(async));
+
+ public override async Task Join_local_collection_int_closure_is_cached_correctly(bool async)
+ {
+ var ids = new uint[] { 1, 2 };
+
+ await AssertTranslationFailed(
+ () => AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID));
+
+ ids = new uint[] { 3 };
+ await AssertTranslationFailed(
+ () => AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID));
+ }
}
diff --git a/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs
index 67ce682a73e..180f02494c2 100644
--- a/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs
@@ -13,6 +13,25 @@ public QueryFilterFuncletizationInMemoryTest(
{
}
+ public override void DbContext_list_is_parameterized()
+ {
+ using var context = CreateContext();
+ // Default value of TenantIds is null InExpression over null values throws
+ Assert.Throws(() => context.Set().ToList());
+
+ context.TenantIds = new List();
+ var query = context.Set().ToList();
+ Assert.Empty(query);
+
+ context.TenantIds = new List { 1 };
+ query = context.Set().ToList();
+ Assert.Single(query);
+
+ context.TenantIds = new List { 2, 3 };
+ query = context.Set().ToList();
+ Assert.Equal(2, query.Count);
+ }
+
public class QueryFilterFuncletizationInMemoryFixture : QueryFilterFuncletizationFixtureBase
{
protected override ITestStoreFactory TestStoreFactory
diff --git a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs
new file mode 100644
index 00000000000..19d33e77f10
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs
@@ -0,0 +1,55 @@
+// 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;
+
+public abstract class NonSharedPrimitiveCollectionsQueryRelationalTestBase : NonSharedPrimitiveCollectionsQueryTestBase
+{
+ // On relational databases, byte[] gets mapped to a special binary data type, which isn't queryable as a regular primitive collection.
+ [ConditionalFact]
+ public override Task Array_of_byte()
+ => AssertTranslationFailed(() => TestArray((byte)1, (byte)2));
+
+ [ConditionalFact]
+ public virtual async Task Column_collection_inside_json_owned_entity()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity().OwnsOne(t => t.Owned, b => b.ToJson()),
+ seed: context =>
+ {
+ context.AddRange(
+ new TestOwner { Owned = new TestOwned { Strings = new[] { "foo", "bar" } } },
+ new TestOwner { Owned = new TestOwned { Strings = new[] { "baz" } } });
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var result = await context.Set().SingleAsync(o => o.Owned.Strings.Count() == 2);
+ Assert.Equivalent(new[] { "foo", "bar" }, result.Owned.Strings);
+
+ result = await context.Set().SingleAsync(o => o.Owned.Strings[1] == "bar");
+ Assert.Equivalent(new[] { "foo", "bar" }, result.Owned.Strings);
+ }
+
+ protected class TestOwner
+ {
+ public int Id { get; set; }
+ public TestOwned Owned { get; set; }
+ }
+
+ [Owned]
+ protected class TestOwned
+ {
+ public string[] Strings { get; set; }
+ }
+
+ protected TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ protected void ClearLog()
+ => TestSqlLoggerFactory.Clear();
+
+ protected void AssertSql(params string[] expected)
+ => TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs
index e63dffbd772..c779cb7ed2f 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs
@@ -108,29 +108,6 @@ public virtual void Throws_when_subquery_main_from_clause()
CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer)));
}
- [ConditionalFact]
- public virtual void Throws_when_select_many()
- {
- using var context = CreateContext();
-
- AssertTranslationFailed(
- () => (from c1 in context.Customers
- from i in new[] { 1, 2, 3 }
- select c1)
- .ToList());
- }
-
- [ConditionalFact]
- public virtual void Throws_when_join()
- {
- using var context = CreateContext();
- AssertTranslationFailed(
- () => (from e1 in context.Employees
- join i in new uint[] { 1, 2, 3 } on e1.EmployeeID equals i
- select e1)
- .ToList());
- }
-
[ConditionalFact]
public virtual void Does_not_throws_when_group_join()
{
diff --git a/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs
new file mode 100644
index 00000000000..4780f98ee64
--- /dev/null
+++ b/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs
@@ -0,0 +1,292 @@
+// 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;
+
+public abstract class NonSharedPrimitiveCollectionsQueryTestBase : NonSharedModelTestBase
+{
+ #region Support for specific element types
+
+ [ConditionalFact]
+ public virtual Task Array_of_string()
+ => TestArray("a", "b");
+
+ [ConditionalFact]
+ public virtual Task Array_of_int()
+ => TestArray(1, 2);
+
+ [ConditionalFact]
+ public virtual Task Array_of_long()
+ => TestArray(1L, 2L);
+
+ [ConditionalFact]
+ public virtual Task Array_of_short()
+ => TestArray((short)1, (short)2);
+
+ [ConditionalFact]
+ public virtual Task Array_of_byte()
+ => TestArray((byte)1, (byte)2);
+
+ [ConditionalFact]
+ public virtual Task Array_of_double()
+ => TestArray(1d, 2d);
+
+ [ConditionalFact]
+ public virtual Task Array_of_float()
+ => TestArray(1f, 2f);
+
+ [ConditionalFact]
+ public virtual Task Array_of_decimal()
+ => TestArray(1m, 2m);
+
+ [ConditionalFact]
+ public virtual Task Array_of_DateTime()
+ => TestArray(new DateTime(2023, 1, 1, 12, 30, 0), new DateTime(2023, 1, 2, 12, 30, 0));
+
+ [ConditionalFact]
+ public virtual Task Array_of_DateOnly()
+ => TestArray(new DateOnly(2023, 1, 1), new DateOnly(2023, 1, 2));
+
+ [ConditionalFact]
+ public virtual Task Array_of_TimeOnly()
+ => TestArray(new TimeOnly(12, 30, 0), new TimeOnly(12, 30, 1));
+
+ [ConditionalFact]
+ public virtual Task Array_of_DateTimeOffset()
+ => TestArray(
+ new DateTimeOffset(2023, 1, 1, 12, 30, 0, TimeSpan.FromHours(2)),
+ new DateTimeOffset(2023, 1, 2, 12, 30, 0, TimeSpan.FromHours(2)));
+
+ [ConditionalFact]
+ public virtual Task Array_of_bool()
+ => TestArray(true, false);
+
+ [ConditionalFact]
+ public virtual Task Array_of_Guid()
+ => TestArray(
+ new Guid("dc8c903d-d655-4144-a0fd-358099d40ae1"),
+ new Guid("008719a5-1999-4798-9cf3-92a78ffa94a2"));
+
+ [ConditionalFact]
+ public virtual Task Array_of_byte_array()
+ => TestArray(new byte[] { 1, 2 }, new byte[] { 3, 4 });
+
+ [ConditionalFact]
+ public virtual Task Array_of_enum()
+ => TestArray(MyEnum.Label1, MyEnum.Label2);
+
+ enum MyEnum { Label1, Label2 }
+
+ // This ensures that collections of Geometry (e.g. Geometry[]) aren't mapped; NTS has GeometryCollection for that.
+ // See SQL Server/SQLite for a sample implementation.
+ [ConditionalFact] // #30630
+ public abstract Task Array_of_geometry_is_not_supported();
+
+ [ConditionalFact]
+ public virtual async Task Array_of_array_is_not_supported()
+ {
+ var exception = await Assert.ThrowsAsync(() => TestArray(new[] { 1, 2, 3 }, new[] { 4, 5, 6 }));
+ Assert.Equal(CoreStrings.PropertyNotMapped("int[][]", "TestEntity", "SomeArray"), exception.Message);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Multidimensional_array_is_not_supported()
+ {
+ var exception = await Assert.ThrowsAsync(() => InitializeAsync(
+ onModelCreating: mb => mb.Entity().Property(typeof(int[,]), "MultidimensionalArray")));
+ Assert.Equal(CoreStrings.PropertyNotMapped("int[,]", "TestEntity", "MultidimensionalArray"), exception.Message);
+ }
+
+ #endregion Support for specific element types
+
+ [ConditionalFact]
+ public virtual async Task Column_with_custom_converter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity()
+ .Property(m => m.Ints)
+ .HasConversion(
+ i => string.Join(",", i),
+ s => s.Split(",", StringSplitOptions.None).Select(int.Parse).ToArray(),
+ new ValueComparer(favorStructuralComparisons: true)),
+ seed: context =>
+ {
+ context.AddRange(
+ new TestEntity { Id = 1, Ints = new[] { 1, 2, 3 } },
+ new TestEntity { Id = 2, Ints = new[] { 1, 2, 4 } });
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var ints = new[] { 1, 2, 3 };
+ var result = await context.Set().SingleAsync(m => m.Ints == ints);
+ Assert.Equal(1, result.Id);
+
+ // Custom converters allow reading/writing, but not querying, as we have no idea about the internal representation
+ await AssertTranslationFailed(() => context.Set().SingleAsync(m => m.Ints.Length == 2));
+ }
+
+ [ConditionalFact(Skip = "Currently fails because we don't use the element mapping when serializing to JSON, but just do JsonSerializer.Serialize, #30677")]
+ public virtual async Task Parameter_with_inferred_value_converter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity(
+ b =>
+ {
+ b.Property("PropertyWithValueConverter")
+ .HasConversion(w => w.Value, i => new IntWrapper(i));
+ }),
+ seed: context =>
+ {
+ var entry1 = context.Add(new TestEntity { Id = 1 });
+ entry1.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(8);
+ var entry2 = context.Add(new TestEntity { Id = 2 });
+ entry2.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(9);
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var ints = new IntWrapper[] { new(1), new(8) };
+ var result = await context.Set()
+ .SingleAsync(m => ints.Count(i => i == EF.Property(m, "PropertyWithValueConverter")) == 1);
+ Assert.Equal(1, result.Id);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Constant_with_inferred_value_converter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity(
+ b =>
+ {
+ b.Property("PropertyWithValueConverter")
+ .HasConversion(w => w.Value, i => new IntWrapper(i));
+ }),
+ seed: context =>
+ {
+ var entry1 = context.Add(new TestEntity { Id = 1 });
+ entry1.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(8);
+ var entry2 = context.Add(new TestEntity { Id = 2 });
+ entry2.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(9);
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var result = await context.Set()
+ .SingleAsync(
+ m => new IntWrapper[] { new(1), new(8) }.Count(i => i == EF.Property(m, "PropertyWithValueConverter")) == 1);
+ Assert.Equal(1, result.Id);
+ }
+
+ class IntWrapper
+ {
+ public IntWrapper(int value)
+ => Value = value;
+
+ public int Value { get; set; }
+ }
+
+ [ConditionalFact]
+ public virtual async Task Inline_collection_in_query_filter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity().HasQueryFilter(t => new[] { 1, 2, 3 }.Count(i => i > t.Id) == 1),
+ seed: context =>
+ {
+ context.AddRange(
+ new TestEntity { Id = 1 },
+ new TestEntity { Id = 2 });
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var result = await context.Set().SingleAsync();
+ Assert.Equal(2, result.Id);
+ }
+
+ ///
+ /// A utility that allows easy testing of querying out arbitrary element types from a primitive collection, provided two distinct
+ /// element values.
+ ///
+ protected async Task TestArray(
+ TElement value1,
+ TElement value2,
+ Action onModelCreating = null)
+ {
+ var arrayClrType = typeof(TElement).MakeArrayType();
+
+ var contextFactory = await InitializeAsync(
+ onModelCreating: onModelCreating ?? (mb => mb.Entity().Property(arrayClrType, "SomeArray")),
+ seed: context =>
+ {
+ var instance1 = new TestEntity { Id = 1 };
+ context.Add(instance1);
+ var array1 = new TElement[2];
+ array1.SetValue(value1, 0);
+ array1.SetValue(value1, 1);
+ context.Entry(instance1).Property("SomeArray").CurrentValue = array1;
+
+ var instance2 = new TestEntity { Id = 2 };
+ context.Add(instance2);
+ var array2 = new TElement[2];
+ array2.SetValue(value1, 0);
+ array2.SetValue(value2, 1);
+ context.Entry(instance2).Property("SomeArray").CurrentValue = array2;
+
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var entityParam = Expression.Parameter(typeof(TestEntity), "m");
+ var efPropertyCall = Expression.Call(
+ typeof(EF).GetMethod(nameof(EF.Property), BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(arrayClrType),
+ entityParam,
+ Expression.Constant("SomeArray"));
+
+ var elementParam = Expression.Parameter(typeof(TElement), "a");
+ var predicate = Expression.Lambda>(
+ Expression.Equal(
+ Expression.Call(
+ EnumerableMethods.CountWithPredicate.MakeGenericMethod(typeof(TElement)),
+ efPropertyCall,
+ Expression.Lambda(
+ Expression.Equal(elementParam, Expression.Constant(value1)),
+ elementParam)),
+ Expression.Constant(2)),
+ entityParam);
+
+ var result = await context.Set().SingleAsync(predicate);
+ Assert.Equal(1, result.Id);
+ }
+
+ protected class TestContext : DbContext
+ {
+ public TestContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever();
+ }
+
+ protected class TestEntity
+ {
+ public int Id { get; set; }
+ public int[] Ints { get; set; }
+ }
+
+ protected override string StoreName
+ => "NonSharedPrimitiveCollectionsTest";
+
+ protected static async Task AssertTranslationFailed(Func query)
+ => Assert.Contains(
+ CoreStrings.TranslationFailed("")[48..],
+ (await Assert.ThrowsAsync(query))
+ .Message);
+}
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs
index a8514731ae7..9b30b0cbf21 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs
@@ -208,12 +208,12 @@ public virtual void Query_with_array_parameter()
using (var context = CreateContext())
{
- query(context, new[] { "ALFKI" });
+ Assert.Equal(1, query(context, new[] { "ALFKI" }).Count());
}
using (var context = CreateContext())
{
- query(context, new[] { "ANATR" });
+ Assert.Equal(1, query(context, new[] { "ANATR" }).Count());
}
}
@@ -466,12 +466,12 @@ public virtual async Task Query_with_array_parameter_async()
using (var context = CreateContext())
{
- await Enumerate(query(context, new[] { "ALFKI" }));
+ Assert.Equal(1, await CountAsync(query(context, new[] { "ALFKI" })));
}
using (var context = CreateContext())
{
- await Enumerate(query(context, new[] { "ANATR" }));
+ Assert.Equal(1, await CountAsync(query(context, new[] { "ANATR" })));
}
}
@@ -847,27 +847,18 @@ await asyncSingleResultQueryWithCancellationToken(
"CHOPS", "CONSH", default));
}
- [ConditionalFact]
- public virtual void MakeBinary_does_not_throw_for_unsupported_operator()
- {
- var query = EF.CompileQuery(
- (NorthwindContext context, object[] parameters)
- => context.Customers.Where(c => c.CustomerID == (string)parameters[0]));
-
- using var context = CreateContext();
-
- var result = query(context, new[] { "ALFKI" }).ToList();
-
- Assert.Single(result);
- }
-
- protected async Task Enumerate(IAsyncEnumerable source)
+ protected async Task CountAsync(IAsyncEnumerable source)
{
+ var count = 0;
await foreach (var _ in source)
{
+ count++;
}
+ return count;
}
protected NorthwindContext CreateContext()
=> Fixture.CreateContext();
+
+ public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } };
}
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs
index ca678bdefb3..8fa586cf48b 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs
@@ -230,30 +230,25 @@ join o in ss.Set().Where(o => o.OrderID < 10250) on true equals true
public virtual async Task Join_local_collection_int_closure_is_cached_correctly(bool async)
{
var ids = new uint[] { 1, 2 };
- // Join with local collection using TVP. Issue #19016.
- await AssertTranslationFailed(
- () => AssertQueryScalar(
- async,
- ss => from e in ss.Set()
- join id in ids on e.EmployeeID equals id
- select e.EmployeeID));
+ await AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID);
ids = new uint[] { 3 };
- // Join with local collection using TVP. Issue #19016.
- await AssertTranslationFailed(
- () => AssertQueryScalar(
- async,
- ss => from e in ss.Set()
- join id in ids on e.EmployeeID equals id
- select e.EmployeeID));
+ await AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID);
}
- [ConditionalTheory]
+ [ConditionalTheory(Skip = "#30677")]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Join_local_string_closure_is_cached_correctly(bool async)
{
var ids = "12";
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
@@ -262,7 +257,6 @@ join id in ids on e.EmployeeID equals id
select e.EmployeeID));
ids = "3";
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
@@ -271,13 +265,12 @@ join id in ids on e.EmployeeID equals id
select e.EmployeeID));
}
- [ConditionalTheory]
+ [ConditionalTheory(Skip = "#30677")]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Join_local_bytes_closure_is_cached_correctly(bool async)
{
var ids = new byte[] { 1, 2 };
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
@@ -286,7 +279,6 @@ join id in ids on e.EmployeeID equals id
select e.EmployeeID));
ids = new byte[] { 3 };
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
new file mode 100644
index 00000000000..199ae52ce07
--- /dev/null
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -0,0 +1,734 @@
+// 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;
+
+public class PrimitiveCollectionsQueryTestBase : QueryTestBase
+ where TFixture : PrimitiveCollectionsQueryTestBase.PrimitiveCollectionsQueryFixtureBase, new()
+{
+ protected PrimitiveCollectionsQueryTestBase(TFixture fixture)
+ : base(fixture)
+ {
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_of_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 10, 999 }.Contains(c.Int)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_of_nullable_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new int?[] { 10, 999 }.Contains(c.NullableInt)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_of_nullable_ints_Contains_null(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new int?[] { null, 999 }.Contains(c.NullableInt)),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_zero_values(bool async)
+ => AssertQuery(
+ async,
+ // ReSharper disable once UseArrayEmptyMethod
+ ss => ss.Set().Where(c => new int[0].Count(i => i > c.Id) == 1),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_one_value(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2 }.Count(i => i > c.Id) == 1),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_two_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999 }.Count(i => i > c.Id) == 1),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_three_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Count(i => i > c.Id) == 2),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_zero_values(bool async)
+ => AssertQuery(
+ async,
+ // ReSharper disable once UseArrayEmptyMethod
+ ss => ss.Set().Where(c => new int[0].Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_one_value(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2 }.Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_two_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999 }.Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_three_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_all_parameters(bool async)
+ {
+ var (i, j) = (2, 999);
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { i, j }.Contains(c.Id)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async)
+ {
+ var i = 2;
+
+ await AssertTranslationFailed(
+ () => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { i, c.Int }.Contains(c.Id)),
+ entryCount: 1));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_Count(bool async)
+ {
+ var ids = new[] { 2, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ids.Count(i => i > c.Id) == 1),
+ entryCount: 2);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_ints_Contains(bool async)
+ {
+ var ints = new[] { 10, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints.Contains(c.Int)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_nullable_ints_Contains(bool async)
+ {
+ var nullableInts = new int?[] { 10, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ var nullableInts = new int?[] { null, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt)),
+ entryCount: 2);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_strings_Contains(bool async)
+ {
+ var strings = new[] { "10", "999" };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => strings.Contains(c.String)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_DateTimes_Contains(bool async)
+ {
+ var dateTimes = new[]
+ {
+ new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc),
+ new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => dateTimes.Contains(c.DateTime)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_bools_Contains(bool async)
+ {
+ var bools = new[] { true };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => bools.Contains(c.Bool)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_enums_Contains(bool async)
+ {
+ var enums = new[] { MyEnum.Value1, MyEnum.Value4 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => enums.Contains(c.Enum)),
+ entryCount: 2);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Parameter_collection_null_Contains(bool async)
+ {
+ int[] ints = null;
+
+ await AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints.Contains(c.Int)),
+ ss => ss.Set().Where(c => false));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Contains(10)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_nullable_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.NullableInts.Contains(10)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_nullable_ints_Contains_null(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.NullableInts.Contains(null)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_bools_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Bools.Contains(true)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Count_method(bool async)
+ => AssertQuery(
+ async,
+ // ReSharper disable once UseCollectionCountProperty
+ ss => ss.Set().Where(c => c.Ints.Count() == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Length(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Length == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_int(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints[1] == 10),
+ ss => ss.Set().Where(c => (c.Ints.Length >= 2 ? c.Ints[1] : -1) == 10),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_string(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Strings[1] == "10"),
+ ss => ss.Set().Where(c => (c.Strings.Length >= 2 ? c.Strings[1] : "-1") == "10"),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_datetime(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(
+ c => c.DateTimes[1] == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)),
+ ss => ss.Set().Where(
+ c => (c.DateTimes.Length >= 2 ? c.DateTimes[1] : default) == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_beyond_end(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints[999] == 10),
+ ss => ss.Set().Where(c => false),
+ entryCount: 0);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_index_Column(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 1, 2, 3 }[c.Int] == 1),
+ ss => ss.Set().Where(c => (c.Int <= 2 ? new[] { 1, 2, 3 }[c.Int] : -1) == 1),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_index_Column(bool async)
+ {
+ var ints = new[] { 1, 2, 3 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints[c.Int] == 1),
+ ss => ss.Set().Where(c => (c.Int <= 2 ? ints[c.Int] : -1) == 1),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_ElementAt(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.ElementAt(1) == 10),
+ ss => ss.Set().Where(c => (c.Ints.Length >= 2 ? c.Ints.ElementAt(1) : -1) == 10),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Skip(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Skip(1).Count() == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Take(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Take(2).Contains(11)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Skip_Take(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Skip(1).Take(2).Contains(11)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Any(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Any()),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_projection_from_top_level(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().OrderBy(c => c.Id).Select(c => c.Ints),
+ elementAsserter: (a, b) => Assert.Equivalent(a, b),
+ assertOrder: true);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_and_parameter_collection_Join(bool async)
+ {
+ var ints = new[] { 11, 111 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Join(ints, i => i, j => j, (i, j) => new { I = i, J = j }).Count() == 2),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_Concat_column_collection(bool async)
+ {
+ var ints = new[] { 11, 111 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints.Concat(c.Ints).Count() == 2),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Union_parameter_collection(bool async)
+ {
+ var ints = new[] { 11, 111 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Union(ints).Count() == 2),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Intersect_inline_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Intersect(new[] { 11, 111 }).Count() == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Except_column_collection(bool async)
+ // Note that since the VALUES is on the left side of the set operation, it must assign column names, otherwise the column coming
+ // out of the set operation has undetermined naming.
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(
+ c => new[] { 11, 111 }.Except(c.Ints).Count(i => i % 2 == 1) == 2),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_equality_parameter_collection(bool async)
+ {
+ var ints = new[] { 1, 10 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints == ints),
+ ss => ss.Set().Where(c => c.Ints.SequenceEqual(ints)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async)
+ {
+ var ints = new[] { 1, 10 };
+
+ await AssertTranslationFailed(
+ () => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Concat(ints) == new[] { 1, 11, 111, 1, 10 })));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_equality_inline_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints == new[] { 1, 10 }),
+ ss => ss.Set().Where(c => c.Ints.SequenceEqual(new[] { 1, 10 })),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
+ {
+ // The Skip causes a pushdown into a subquery before the Union, and so the projection on the left side of the union points to the
+ // subquery as its table, and not directly to the parameter's table.
+ // This creates an initially untyped ColumnExpression referencing the pushed-down subquery; it must also be inferred.
+ // Note that this must be a compiled query, since with normal queries the Skip(1) gets client-evaluated.
+ // TODO:
+ var compiledQuery = EF.CompileQuery(
+ (PrimitiveCollectionsContext context, int[] ints)
+ => context.Set().Where(p => ints.Skip(1).Count(i => i > p.Id) == 1).Count());
+
+ await using var context = Fixture.CreateContext();
+ var ints = new[] { 10, 111 };
+
+ // TODO: Complete
+ var results = compiledQuery(context, ints);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
+ {
+ // The Skip causes a pushdown into a subquery before the Union, and so the projection on the left side of the union points to the
+ // subquery as its table, and not directly to the parameter's table.
+ // This creates an initially untyped ColumnExpression referencing the pushed-down subquery; it must also be inferred.
+ // Note that this must be a compiled query, since with normal queries the Skip(1) gets client-evaluated.
+ var compiledQuery = EF.CompileQuery(
+ (PrimitiveCollectionsContext context, int[] ints)
+ => context.Set().Where(p => ints.Skip(1).Union(p.Ints).Count() == 3));
+
+ await using var context = Fixture.CreateContext();
+ var ints = new[] { 10, 111 };
+
+ // TODO: Complete
+ var results = compiledQuery(context, ints).ToList();
+ }
+
+ [ConditionalFact]
+ public virtual void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
+ {
+ // The array indexing is translated as a subquery over e.g. OpenJson with LIMIT/OFFSET.
+ // Since there's a CAST over that, the type mapping inference from the other side (p.String) doesn't propagate inside to the
+ // subquery. In this case, the CAST operand gets the default CLR type mapping, but that's object in this case.
+ // We should apply the default type mapping to the parameter, but need to figure out the exact rules when to do this.
+ var query = EF.CompileQuery(
+ (PrimitiveCollectionsContext context, object[] parameters)
+ => context.Set().Where(p => p.String == (string)parameters[0]));
+
+ using var context = Fixture.CreateContext();
+
+ var exception = Assert.Throws(() => query(context, new[] { "foo" }).ToList());
+
+ Assert.Contains("in the SQL tree does not have a type mapping assigned", exception.Message);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_in_subquery_Union_parameter_collection(bool async)
+ {
+ var ints = new[] { 10, 111 };
+
+ // The Skip causes a pushdown into a subquery before the Union. This creates an initially untyped ColumnExpression referencing the
+ // pushed-down subquery; it must also be inferred
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Skip(1).Union(ints).Count() == 3),
+ entryCount: 1);
+ }
+
+ public abstract class PrimitiveCollectionsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase
+ {
+ private PrimitiveArrayData _expectedData;
+
+ protected override string StoreName
+ => "PrimitiveCollectionsTest";
+
+ public Func GetContextCreator()
+ => () => CreateContext();
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(c => c.Log(CoreEventId.DistinctAfterOrderByWithoutRowLimitingOperatorWarning));
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
+ => modelBuilder.Entity().Property(p => p.Id).ValueGeneratedNever();
+
+ protected override void Seed(PrimitiveCollectionsContext context)
+ => new PrimitiveArrayData(context);
+
+ public virtual ISetSource GetExpectedData()
+ => _expectedData ??= new PrimitiveArrayData();
+
+ public IReadOnlyDictionary EntitySorters { get; } = new Dictionary>
+ {
+ { typeof(PrimitiveCollectionsEntity), e => ((PrimitiveCollectionsEntity)e)?.Id }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+
+ public IReadOnlyDictionary EntityAsserters { get; } = new Dictionary>
+ {
+ {
+ typeof(PrimitiveCollectionsEntity), (e, a) =>
+ {
+ Assert.Equal(e == null, a == null);
+
+ if (a != null)
+ {
+ var ee = (PrimitiveCollectionsEntity)e;
+ var aa = (PrimitiveCollectionsEntity)a;
+
+ Assert.Equal(ee.Id, aa.Id);
+ Assert.Equivalent(ee.Ints, aa.Ints, strict: true);
+ Assert.Equivalent(ee.Strings, aa.Strings, strict: true);
+ Assert.Equivalent(ee.DateTimes, aa.DateTimes, strict: true);
+ Assert.Equivalent(ee.Bools, aa.Bools, strict: true);
+ // TODO: Complete
+ }
+ }
+ }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+ }
+
+ public class PrimitiveCollectionsContext : PoolableDbContext
+ {
+ public PrimitiveCollectionsContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+ }
+
+ public class PrimitiveCollectionsEntity
+ {
+ public int Id { get; set; }
+
+ public string String { get; set; }
+ public int Int { get; set; }
+ public DateTime DateTime { get; set; }
+ public bool Bool { get; set; }
+ public MyEnum Enum { get; set; }
+ public int? NullableInt { get; set; }
+
+ public string[] Strings { get; set; }
+ public int[] Ints { get; set; }
+ public DateTime[] DateTimes { get; set; }
+ public bool[] Bools { get; set; }
+ public MyEnum[] Enums { get; set; }
+ public int?[] NullableInts { get; set; }
+ }
+
+ public enum MyEnum { Value1, Value2, Value3, Value4 }
+
+ public class PrimitiveArrayData : ISetSource
+ {
+ public IReadOnlyList PrimitiveArrayEntities { get; }
+
+ public PrimitiveArrayData(PrimitiveCollectionsContext context = null)
+ {
+ PrimitiveArrayEntities = CreatePrimitiveArrayEntities();
+
+ if (context != null)
+ {
+ context.AddRange(PrimitiveArrayEntities);
+ context.SaveChanges();
+ }
+ }
+
+ public IQueryable Set()
+ where TEntity : class
+ {
+ if (typeof(TEntity) == typeof(PrimitiveCollectionsEntity))
+ {
+ return (IQueryable)PrimitiveArrayEntities.AsQueryable();
+ }
+
+ throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity));
+ }
+
+ private static IReadOnlyList CreatePrimitiveArrayEntities()
+ => new List
+ {
+ new()
+ {
+ Id = 1,
+
+ Int = 10,
+ String = "10",
+ DateTime = new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc),
+ Bool = true,
+ Enum = MyEnum.Value1,
+ NullableInt = 10,
+
+ Ints = new[] { 1, 10 },
+ Strings = new[] { "1", "10" },
+ DateTimes = new DateTime[]
+ {
+ new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc),
+ new(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)
+ },
+ Bools = new[] { true, false },
+ Enums = new[] { MyEnum.Value1, MyEnum.Value2 },
+ NullableInts = new int?[] { 1, 10 },
+ },
+ new()
+ {
+ Id = 2,
+
+ Int = 11,
+ String = "11",
+ DateTime = new DateTime(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc),
+ Bool = false,
+ Enum = MyEnum.Value2,
+ NullableInt = null,
+
+ Ints = new[] { 1, 11, 111 },
+ Strings = new[] { "1", "11", "111" },
+ DateTimes = new DateTime[]
+ {
+ new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc),
+ new(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc),
+ new(2020, 1, 31, 12, 30, 0, DateTimeKind.Utc)
+ },
+ Bools = new[] { false },
+ Enums = new[] { MyEnum.Value2, MyEnum.Value3 },
+ NullableInts = new int?[] { 1, 11, null },
+ },
+ new()
+ {
+ Id = 3,
+
+ Int = 0,
+ String = "",
+ DateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ Bool = false,
+ Enum = MyEnum.Value1,
+ NullableInt = null,
+
+ Ints = Array.Empty(),
+ Strings = Array.Empty(),
+ DateTimes = Array.Empty(),
+ Bools = Array.Empty(),
+ Enums = Array.Empty(),
+ NullableInts = Array.Empty(),
+ }
+ };
+ }
+}
diff --git a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs
index 45fc352f56a..822cacbd7e8 100644
--- a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs
@@ -1165,6 +1165,12 @@ protected static async Task AssertTranslationFailed(Func query)
(await Assert.ThrowsAsync(query))
.Message);
+ protected static void AssertTranslationFailed(Action query)
+ => Assert.Contains(
+ CoreStrings.TranslationFailed("")[48..],
+ Assert.Throws(query)
+ .Message);
+
protected static async Task AssertTranslationFailedWithDetails(Func query, string details)
=> Assert.Contains(
CoreStrings.TranslationFailedWithDetails("", details)[21..],
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs
index e9c32a9894f..ea1a611bc6e 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs
@@ -1214,6 +1214,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [l0].[Id] IS NULL THEN 0
ELSE [l0].[Id]
@@ -1221,7 +1223,10 @@ ELSE [l0].[Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id]
LEFT JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [l0].[Id]
""");
}
@@ -2325,17 +2330,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t].[Date], [t0].[Id]
FROM (
SELECT [l].[Date]
FROM [LevelOne] AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
LEFT JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [LevelOne] AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
index f7041ff82ca..fd5290ec280 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
@@ -2848,6 +2848,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [t0].[OneToOne_Required_PK_Date] IS NULL OR [t0].[Level1_Required_Id] IS NULL OR [t0].[OneToMany_Required_Inverse2Id] IS NULL THEN 0
WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t0].[Id0]
@@ -2872,7 +2874,10 @@ WHERE [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse
) AS [t1] ON CASE
WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t0].[Id0]
END = [t1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [t0].[Id], [t0].[Id0]
""");
}
@@ -3009,17 +3014,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t].[Date], [t0].[Id]
FROM (
SELECT [l].[Date]
FROM [Level1] AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
LEFT JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [Level1] AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs
index ffc5d625c4d..8cb7b155829 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs
@@ -3187,22 +3187,32 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [l0].[Id] IS NULL THEN 0
ELSE [l0].[Id]
END, [l].[Id], [l0].[Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [l0].[Id]
""",
//
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [l1].[Id], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id], [l].[Id], [l0].[Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id]
INNER JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [l0].[Id]
""");
}
@@ -3734,25 +3744,38 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [l].[Date]
FROM [LevelOne] AS [l]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
ORDER BY [l].[Date]
""",
//
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t0].[Id], [t].[Date]
FROM (
SELECT [l].[Date]
FROM [LevelOne] AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
INNER JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [LevelOne] AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs
index d55fd460371..02910614534 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs
@@ -3076,10 +3076,15 @@ public override async Task Accessing_optional_property_inside_result_operator_su
AssertSql(
"""
+@__names_0='["Name1","Name2"]' (Size = 4000)
+
SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id]
-WHERE [l0].[Name] NOT IN (N'Name1', N'Name2') OR [l0].[Name] IS NULL
+WHERE NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__names_0) AS [n]
+ WHERE ([l0].[Name] = [n].[value] AND [l0].[Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([l0].[Name] IS NULL AND [n].[value] IS NULL))
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs
index 1c2cbc0bf24..35913c190f3 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs
@@ -5403,6 +5403,8 @@ public override async Task Accessing_optional_property_inside_result_operator_su
AssertSql(
"""
+@__names_0='["Name1","Name2"]' (Size = 4000)
+
SELECT [l].[Id], [l].[Date], [l].[Name]
FROM [Level1] AS [l]
LEFT JOIN (
@@ -5410,7 +5412,10 @@ LEFT JOIN (
FROM [Level1] AS [l0]
WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL
) AS [t] ON [l].[Id] = [t].[Level1_Optional_Id]
-WHERE [t].[Level2_Name] NOT IN (N'Name1', N'Name2') OR [t].[Level2_Name] IS NULL
+WHERE NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__names_0) AS [n]
+ WHERE ([t].[Level2_Name] = [n].[value] AND [t].[Level2_Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([t].[Level2_Name] IS NULL AND [n].[value] IS NULL))
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
index ce029b62c64..3dbb7320e7f 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
@@ -211,10 +211,15 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Gears] AS [g]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -229,11 +234,16 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Gears] AS [g]
INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [c].[Location] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [c].[Location] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -248,10 +258,15 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -2085,9 +2100,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n
AssertSql(
"""
+@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000)
+
SELECT [c].[Name], [c].[Location], [c].[Nation]
FROM [Cities] AS [c]
-WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[value] IS NULL AND [c].[Location] IS NULL))
""");
}
@@ -3082,9 +3102,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a
AssertSql(
"""
+@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000)
+
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Tags] AS [t]
-WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS uniqueidentifier) = [t].[Id])
""");
}
@@ -3600,10 +3625,15 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool
AssertSql(
"""
+@__cities_0='["Ephyra",null]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name]
-WHERE [g].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL)
+WHERE [g].[SquadId] < 2 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[value] IS NULL AND [c].[Name] IS NULL))
""");
}
@@ -5886,10 +5916,18 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz
AssertSql(
"""
+@__nicknames_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [w].[Name], [w].[Id]
FROM [Gears] AS [g]
LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName]
-ORDER BY [g].[Nickname], [g].[SquadId]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__nicknames_0) AS [n]
+ WHERE CAST([n].[value] AS nvarchar(450)) = [g].[Nickname]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END DESC, [g].[Nickname], [g].[SquadId]
""");
}
@@ -6625,10 +6663,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a
"""
@__start_0='1902-01-01T10:00:00.1234567+01:30'
@__end_1='1902-01-03T10:00:00.1234567+01:30'
+@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000)
SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Date], [m].[Duration], [m].[Rating], [m].[Time], [m].[Timeline]
FROM [Missions] AS [m]
-WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'
+WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dates_2) AS [d]
+ WHERE CAST([d].[value] AS datetimeoffset) = [m].[Timeline])
""");
}
@@ -7335,8 +7377,17 @@ public override async Task OrderBy_Contains_empty_list(bool async)
AssertSql(
"""
+@__ids_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [g].[SquadId]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""");
}
@@ -8087,10 +8138,15 @@ public override async Task Enum_array_contains(bool async)
AssertSql(
"""
+@__types_0='[null,1]' (Size = 4000)
+
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
LEFT JOIN [Weapons] AS [w0] ON [w].[SynergyWithId] = [w0].[Id]
-WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL)
+WHERE [w0].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__types_0) AS [t]
+ WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[value] IS NULL AND [w0].[AmmunitionType] IS NULL))
""");
}
@@ -9319,9 +9375,14 @@ public override async Task Where_bool_column_and_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
@@ -9331,9 +9392,14 @@ public override async Task Where_bool_column_or_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs
new file mode 100644
index 00000000000..947b7f192c6
--- /dev/null
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs
@@ -0,0 +1,348 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using NetTopologySuite.Geometries;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class NonSharedPrimitiveCollectionsQuerySqlServerTest : NonSharedPrimitiveCollectionsQueryRelationalTestBase
+{
+ #region Support for specific element types
+
+ public override async Task Array_of_int()
+ {
+ await base.Array_of_int();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS int) = 1) = 2
+""");
+ }
+
+ public override async Task Array_of_long()
+ {
+ await base.Array_of_long();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS bigint) = CAST(1 AS bigint)) = 2
+""");
+ }
+
+ public override async Task Array_of_short()
+ {
+ await base.Array_of_short();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS smallint) = CAST(1 AS smallint)) = 2
+""");
+ }
+
+ public override async Task Array_of_double()
+ {
+ await base.Array_of_double();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS float) = 1.0E0) = 2
+""");
+ }
+
+ public override async Task Array_of_float()
+ {
+ await base.Array_of_float();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS real) = CAST(1 AS real)) = 2
+""");
+ }
+
+ public override async Task Array_of_decimal()
+ {
+ await base.Array_of_decimal();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS decimal(18,2)) = 1.0) = 2
+""");
+ }
+
+ public override async Task Array_of_DateTime()
+ {
+ await base.Array_of_DateTime();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS datetime2) = '2023-01-01T12:30:00.0000000') = 2
+""");
+ }
+
+ public override async Task Array_of_DateOnly()
+ {
+ await base.Array_of_DateOnly();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS date) = '2023-01-01') = 2
+""");
+ }
+
+ public override async Task Array_of_TimeOnly()
+ {
+ await base.Array_of_TimeOnly();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS time) = '12:30:00') = 2
+""");
+ }
+
+ public override async Task Array_of_DateTimeOffset()
+ {
+ await base.Array_of_DateTimeOffset();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS datetimeoffset) = '2023-01-01T12:30:00.0000000+02:00') = 2
+""");
+ }
+
+ public override async Task Array_of_bool()
+ {
+ await base.Array_of_bool();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS bit) = CAST(1 AS bit)) = 2
+""");
+ }
+
+ public override async Task Array_of_Guid()
+ {
+ await base.Array_of_Guid();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS uniqueidentifier) = 'dc8c903d-d655-4144-a0fd-358099d40ae1') = 2
+""");
+ }
+
+ // The JSON representation for new[] { 1, 2 } is AQI= (base64), this cannot simply be cast to varbinary(max) (0x0102). See #30727.
+ public override Task Array_of_byte_array()
+ => AssertTranslationFailed(() => base.Array_of_byte_array());
+
+ public override async Task Array_of_enum()
+ {
+ await base.Array_of_enum();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS int) = 0) = 2
+""");
+ }
+
+ [ConditionalFact] // #30630
+ public override async Task Array_of_geometry_is_not_supported()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => InitializeAsync(
+ onConfiguring: options => options.UseSqlServer(o => o.UseNetTopologySuite()),
+ addServices: s => s.AddEntityFrameworkSqlServerNetTopologySuite(),
+ onModelCreating: mb => mb.Entity().Property