From cc34368e95eee1f2984f74eddd20de26dae059d0 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 16 May 2026 11:14:39 +0200 Subject: [PATCH 1/4] Fix NullReferenceException in SQL generator for SelectMany with inline array values When using SelectMany with an inline array where the collection values are unreferenced (e.g. e => new[] { "a", "b" }.Select(k => e)), the values in the ValuesExpression had no type mapping inferred from usage context. The postprocessor then wrapped them in a Convert expression with null TypeMapping, causing a NullReferenceException in QuerySqlGenerator.VisitSqlConstant. Fix: In ApplyTypeMappingsOnValuesExpression, when no type mapping is inferred from usage context, fall back to the default type mapping for the value's CLR type. Fixes #38285 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RelationalTypeMappingPostprocessor.cs | 19 ++++++++++++++++--- .../PrimitiveCollectionsQueryTestBase.cs | 5 +++++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 12 ++++++++++++ .../PrimitiveCollectionsQuerySqliteTest.cs | 6 ++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs index c99d57e7f4e..aa6293c362a 100644 --- a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs @@ -151,10 +151,23 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp { var value = rowValue.Values[j]; - if (value.TypeMapping is null - && inferredTypeMappings[j] is { } inferredTypeMapping) + if (value.TypeMapping is null) { - value = _sqlExpressionFactory.ApplyTypeMapping(value, inferredTypeMapping); + if (inferredTypeMappings[j] is { } inferredTypeMapping) + { + value = _sqlExpressionFactory.ApplyTypeMapping(value, inferredTypeMapping); + } + else + { + // No type mapping was inferred from usage context (e.g. SelectMany where the value column isn't + // referenced). Fall back to the default type mapping for the CLR type. + var defaultTypeMapping = RelationalDependencies.TypeMappingSource.FindMapping( + value.Type, QueryCompilationContext.Model); + if (defaultTypeMapping is not null) + { + value = _sqlExpressionFactory.ApplyTypeMapping(value, defaultTypeMapping); + } + } } // We currently add explicit conversions on the first row (but not to the _ord column), to ensure that the inferred diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 6b8a4dda9e8..de2384a5b3f 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -270,6 +270,11 @@ public virtual async Task Inline_collection_in_query_filter() Assert.Equal(2, result.Id); } + [ConditionalFact] // #38285 + public virtual Task Inline_collection_SelectMany_with_unreferenced_collection_value() + => AssertQuery( + ss => ss.Set().SelectMany(e => new[] { "a", "b" }.Select(k => e))); + [ConditionalFact] public virtual Task Parameter_collection_Count() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index d266ab765bf..17b95e7fb8c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -721,6 +721,18 @@ SELECT COUNT(*) """); } + public override async Task Inline_collection_SelectMany_with_unreferenced_collection_value() + { + await base.Inline_collection_SelectMany_with_unreferenced_collection_value(); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +CROSS APPLY (VALUES (CAST(N'a' AS nvarchar(max))), (N'b')) AS [v]([Value]) +"""); + } + public override async Task Parameter_collection_Count() { await base.Parameter_collection_Count(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 3b657bfab60..7a7ab310151 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -2053,6 +2053,12 @@ public override async Task Column_collection_SelectMany() SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync(() => base.Column_collection_SelectMany())).Message); + public override async Task Inline_collection_SelectMany_with_unreferenced_collection_value() + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Inline_collection_SelectMany_with_unreferenced_collection_value())).Message); + public override async Task Column_collection_SelectMany_with_filter() => Assert.Equal( SqliteStrings.ApplyNotSupported, From cf44e7783293b90d22f124ee46d37c8729a2889b Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 16 May 2026 11:37:06 +0200 Subject: [PATCH 2/4] Add null TypeMapping guards and missing test overrides - Add UnreachableException guards in QuerySqlGenerator.VisitSqlConstant and VisitSqlParameter for null TypeMapping, with message instructing users to file a bug report. - Simplify the type mapping fallback in ApplyTypeMappingsOnValuesExpression using coalesce. - Add missing test overrides for OldSqlServer, JsonType, and Cosmos providers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Query/QuerySqlGenerator.cs | 18 ++++++++++++-- .../RelationalTypeMappingPostprocessor.cs | 24 +++++++------------ .../PrimitiveCollectionsQueryCosmosTest.cs | 7 ++++++ ...imitiveCollectionsQueryOldSqlServerTest.cs | 12 ++++++++++ ...veCollectionsQuerySqlServerJsonTypeTest.cs | 12 ++++++++++ 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 9ed58f72694..bb4ec0b48ab 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -651,8 +651,15 @@ protected virtual Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpress /// The for which to generate SQL. protected virtual Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) { + if (sqlConstantExpression.TypeMapping is null) + { + throw new UnreachableException( + $"SqlConstantExpression with value '{sqlConstantExpression.Value}' has no type mapping. " + + "Please file a bug report at https://github.com/dotnet/efcore."); + } + _relationalCommandBuilder - .Append(sqlConstantExpression.TypeMapping!.GenerateSqlLiteral(sqlConstantExpression.Value), sqlConstantExpression.IsSensitive); + .Append(sqlConstantExpression.TypeMapping.GenerateSqlLiteral(sqlConstantExpression.Value), sqlConstantExpression.IsSensitive); return sqlConstantExpression; } @@ -663,6 +670,13 @@ protected virtual Expression VisitSqlConstant(SqlConstantExpression sqlConstantE /// The for which to generate SQL. protected virtual Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) { + if (sqlParameterExpression.TypeMapping is null) + { + throw new UnreachableException( + $"SqlParameterExpression '{sqlParameterExpression.Name}' has no type mapping. " + + "Please file a bug report at https://github.com/dotnet/efcore."); + } + var name = sqlParameterExpression.Name; // Only add the parameter to the command the first time we see its (non-invariant) name, even though we may need to add its @@ -672,7 +686,7 @@ protected virtual Expression VisitSqlParameter(SqlParameterExpression sqlParamet _relationalCommandBuilder.AddParameter( sqlParameterExpression.InvariantName, _sqlGenerationHelper.GenerateParameterName(name), - sqlParameterExpression.TypeMapping!, + sqlParameterExpression.TypeMapping, sqlParameterExpression.IsNullable); _parameterNames.Add(name); } diff --git a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs index aa6293c362a..57902a9b6a8 100644 --- a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs @@ -151,23 +151,15 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp { var value = rowValue.Values[j]; - if (value.TypeMapping is null) + if (value.TypeMapping is null + // Fall back to the default type mapping for the CLR type when no type mapping was inferred + // from usage context (e.g. SelectMany where the value column isn't referenced). + && (inferredTypeMappings[j] + ?? RelationalDependencies.TypeMappingSource.FindMapping( + value.Type, QueryCompilationContext.Model)) + is { } resolvedTypeMapping) { - if (inferredTypeMappings[j] is { } inferredTypeMapping) - { - value = _sqlExpressionFactory.ApplyTypeMapping(value, inferredTypeMapping); - } - else - { - // No type mapping was inferred from usage context (e.g. SelectMany where the value column isn't - // referenced). Fall back to the default type mapping for the CLR type. - var defaultTypeMapping = RelationalDependencies.TypeMappingSource.FindMapping( - value.Type, QueryCompilationContext.Model); - if (defaultTypeMapping is not null) - { - value = _sqlExpressionFactory.ApplyTypeMapping(value, defaultTypeMapping); - } - } + value = _sqlExpressionFactory.ApplyTypeMapping(value, resolvedTypeMapping); } // We currently add explicit conversions on the first row (but not to the _ord column), to ensure that the inferred diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index f08d13fc970..01f3216f983 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -550,6 +550,13 @@ OFFSET 0 LIMIT 2 """); } + public override async Task Inline_collection_SelectMany_with_unreferenced_collection_value() + { + await base.Inline_collection_SelectMany_with_unreferenced_collection_value(); + + AssertSql("PLACEHOLDER"); + } + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/287 (Aggregates over subqueries return null result set) [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override async Task Parameter_collection_Count() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index b0f77b12b83..457feb34b1c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -478,6 +478,18 @@ SELECT COUNT(*) """); } + public override async Task Inline_collection_SelectMany_with_unreferenced_collection_value() + { + await base.Inline_collection_SelectMany_with_unreferenced_collection_value(); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +CROSS APPLY (VALUES (CAST(N'a' AS nvarchar(max))), (N'b')) AS [v]([Value]) +"""); + } + public override async Task Parameter_collection_Count() { await base.Parameter_collection_Count(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 6bfe2463603..59813065a7c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -655,6 +655,18 @@ SELECT COUNT(*) """); } + public override async Task Inline_collection_SelectMany_with_unreferenced_collection_value() + { + await base.Inline_collection_SelectMany_with_unreferenced_collection_value(); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +CROSS APPLY (VALUES (CAST(N'a' AS nvarchar(max))), (N'b')) AS [v]([Value]) +"""); + } + public override async Task Parameter_collection_Count() { await base.Parameter_collection_Count(); From 9dea2b41e8e9aaa9f66c1a0a620de67670c660bf Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 16 May 2026 20:31:29 +0200 Subject: [PATCH 3/4] Fix Cosmos test to expect InvalidOperationException Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Query/PrimitiveCollectionsQueryCosmosTest.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 01f3216f983..970b1d9be67 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -551,11 +551,8 @@ OFFSET 0 LIMIT 2 } public override async Task Inline_collection_SelectMany_with_unreferenced_collection_value() - { - await base.Inline_collection_SelectMany_with_unreferenced_collection_value(); - - AssertSql("PLACEHOLDER"); - } + => await Assert.ThrowsAsync( + () => base.Inline_collection_SelectMany_with_unreferenced_collection_value()); // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/287 (Aggregates over subqueries return null result set) [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] From 66bf9ea99792777f1522a99a178b25c182c89da8 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 16 May 2026 20:46:17 +0200 Subject: [PATCH 4/4] Remove constant value from UnreachableException message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/EFCore.Relational/Query/QuerySqlGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index bb4ec0b48ab..45c4d5197f6 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -654,7 +654,7 @@ protected virtual Expression VisitSqlConstant(SqlConstantExpression sqlConstantE if (sqlConstantExpression.TypeMapping is null) { throw new UnreachableException( - $"SqlConstantExpression with value '{sqlConstantExpression.Value}' has no type mapping. " + "SqlConstantExpression has no type mapping. " + "Please file a bug report at https://github.com/dotnet/efcore."); }