From fa2b9964d290daca65c1feabfff8e51281ad509b Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:41:59 +0000 Subject: [PATCH 1/6] Update dependencies from build 310667 (#38125) [release/10.0] Source code updates from dotnet/dotnet --- NuGet.config | 3 ++- eng/Version.Details.props | 10 ++++---- eng/Version.Details.xml | 48 +++++++++++++++++++-------------------- global.json | 4 ++-- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/NuGet.config b/NuGet.config index 91178b5124b..ae206c013ea 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,7 +4,8 @@ - + + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 2e499104c85..ce819b05b4e 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,9 +6,9 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26214.110 - 10.0.0-beta.26214.110 - 10.0.0-beta.26214.110 + 10.0.0-beta.26215.122 + 10.0.0-beta.26215.122 + 10.0.0-beta.26215.122 10.0.7 10.0.7 10.0.7 @@ -16,10 +16,10 @@ This file should be imported by eng/Versions.props 10.0.7 10.0.7 10.0.7 - 10.0.7-servicing.26214.110 + 10.0.7-servicing.26215.122 10.0.7 10.0.7 - 10.0.7-servicing.26214.110 + 10.0.7-servicing.26215.122 10.0.7 10.0.7 10.0.7 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c97299089c5..99c11a15937 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,80 +1,80 @@ - + https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 - + https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 - + https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 - + https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 - + https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 - + https://github.com/dotnet/dotnet - ded58554b388029908e68f3a3bb70e1c21b8ebee + 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 diff --git a/global.json b/global.json index 4dd2c43f3c0..e66ed628d1a 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26214.110", - "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26214.110" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26215.122", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26215.122" } } From 8605c14f0297651e14060d50b7e6f84dfd2e430f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:36:34 +0000 Subject: [PATCH 2/6] [release/10.0] Source code updates from dotnet/dotnet (#38127) [release/10.0] Source code updates from dotnet/dotnet --- NuGet.config | 3 +-- eng/Version.Details.props | 10 ++++---- eng/Version.Details.xml | 48 +++++++++++++++++++-------------------- global.json | 8 +++---- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/NuGet.config b/NuGet.config index ae206c013ea..c78ab02d13e 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,8 +4,7 @@ - - + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index ce819b05b4e..f3736170de9 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,9 +6,9 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26215.122 - 10.0.0-beta.26215.122 - 10.0.0-beta.26215.122 + 10.0.0-beta.26216.111 + 10.0.0-beta.26216.111 + 10.0.0-beta.26216.111 10.0.7 10.0.7 10.0.7 @@ -16,10 +16,10 @@ This file should be imported by eng/Versions.props 10.0.7 10.0.7 10.0.7 - 10.0.7-servicing.26215.122 + 10.0.7-servicing.26216.111 10.0.7 10.0.7 - 10.0.7-servicing.26215.122 + 10.0.7-servicing.26216.111 10.0.7 10.0.7 10.0.7 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 99c11a15937..17dec7667e0 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,80 +1,80 @@ - + https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada - + https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada - + https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada - + https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada - + https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada - + https://github.com/dotnet/dotnet - 884fcaedb08b93d224306cea1ad17ffb7f6cd9d3 + d5a73cb9564de5e77882721fc7a85c83fad55ada diff --git a/global.json b/global.json index e66ed628d1a..cd6a76bbfd6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.105", + "version": "10.0.106", "allowPrerelease": true, "rollForward": "latestMajor", "paths": [ @@ -10,7 +10,7 @@ "errorMessage": "The required .NET SDK wasn't found. Please run ./restore.sh or .\\restore.cmd to install it." }, "tools": { - "dotnet": "10.0.105", + "dotnet": "10.0.106", "runtimes": { "dotnet": [ "$(MicrosoftNETCorePlatformsVersion)" @@ -18,7 +18,7 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26215.122", - "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26215.122" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26216.111", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26216.111" } } From ffaad9161c2e6ea731e1d0c926582b225fd8022a Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:10:37 +0000 Subject: [PATCH 3/6] [release/10.0] Source code updates from dotnet/dotnet (#38143) [release/10.0] Source code updates from dotnet/dotnet --- NuGet.config | 2 +- eng/Version.Details.props | 36 +++++++++---------- eng/Version.Details.xml | 74 +++++++++++++++++++-------------------- eng/Versions.props | 2 +- global.json | 4 +-- 5 files changed, 59 insertions(+), 59 deletions(-) diff --git a/NuGet.config b/NuGet.config index c78ab02d13e..e9a0c968c64 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,7 +4,7 @@ - + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index f3736170de9..2b1ab07a695 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,24 +6,24 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26216.111 - 10.0.0-beta.26216.111 - 10.0.0-beta.26216.111 - 10.0.7 - 10.0.7 - 10.0.7 - 10.0.7 - 10.0.7 - 10.0.7 - 10.0.7 - 10.0.7-servicing.26216.111 - 10.0.7 - 10.0.7 - 10.0.7-servicing.26216.111 - 10.0.7 - 10.0.7 - 10.0.7 - 10.0.7 + 10.0.0-beta.26217.115 + 10.0.0-beta.26217.115 + 10.0.0-beta.26217.115 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8-servicing.26217.115 + 10.0.8 + 10.0.8 + 10.0.8-servicing.26217.115 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 17dec7667e0..cfd98161757 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,80 +1,80 @@ - + - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 - + https://github.com/dotnet/dotnet - d5a73cb9564de5e77882721fc7a85c83fad55ada + 71d1809a66d8aacbe21208baeb0bbc484191b3d1 diff --git a/eng/Versions.props b/eng/Versions.props index 8dba19ed039..3696a265b6d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,7 +1,7 @@ - 10.0.7 + 10.0.8 servicing False diff --git a/global.json b/global.json index cd6a76bbfd6..03f2aa1277f 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26216.111", - "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26216.111" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26217.115", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26217.115" } } From ee8b6bf89b663f4856b1f739f47479a3965830d4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 20 Apr 2026 23:40:58 +0200 Subject: [PATCH 4/6] [release/10.0] Fix query filter parameter names with primary constructor parameters (#38136) When a query filter references a primary constructor parameter (e.g. tenantId), the compiler stores it as a field named P. The query filter parameter naming code used this raw Member.Name, producing invalid SQL parameter names containing angle brackets (e.g. @ef_filter__P). Apply SanitizeCompilerGeneratedName to the member name in the query filter parameter naming path, extracting the user-provided name from within the angle brackets (e.g. tenantId). Also port the general safety check from main that catches any remaining problematic characters. Fixes #38132 --- .../Internal/ExpressionTreeFuncletizer.cs | 20 ++++++++++- .../Query/AdHocQueryFiltersQueryTestBase.cs | 35 +++++++++++++++++++ .../AdHocQueryFiltersQuerySqlServerTest.cs | 14 ++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index 45e6f36c87b..39759f5b6ec 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -31,6 +31,9 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor private static readonly bool UseOldBehavior37974 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37974", out var enabled37974) && enabled37974; + private static readonly bool UseOldBehavior38132 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38132", out var enabled38132) && enabled38132; + // The general algorithm here is the following. // 1. First, for each node type, visit that node's children and get their states (evaluatable, contains evaluatable, no evaluatable). // 2. Calculate the parent node's aggregate state from its children; a container node whose children are all evaluatable is itself @@ -2129,6 +2132,19 @@ bool PreserveConvertNode(Expression expression) } } + if (!UseOldBehavior38132) + { + // As a safety guard, if there's any problematic character in the name for any reason, fall back to "p". + foreach (var c in parameterName) + { + if (!char.IsLetterOrDigit(c) && c != '_') + { + parameterName = "p"; + break; + } + } + } + if (UseOldBehavior37152) { // Uniquify the parameter name @@ -2169,7 +2185,9 @@ bool PreserveConvertNode(Expression expression) if (visited != expression) { parameterName = QueryFilterPrefix - + (RemoveConvert(expression) is MemberExpression { Member.Name: var memberName } ? ("__" + memberName) : "__p"); + + (RemoveConvert(expression) is MemberExpression { Member.Name: var memberName } + ? "__" + (UseOldBehavior38132 ? memberName : SanitizeCompilerGeneratedName(memberName)) + : "__p"); isContextAccessor = true; // Context accessors (query filters accessing the context) never get constantized diff --git a/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs index c84d80fef18..a82b0b5d86b 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs @@ -815,4 +815,39 @@ public class FooBar35111 } #endregion + + #region 38132 + + [ConditionalFact] + public virtual async Task Query_filter_with_primary_constructor_parameter() + { + var contextFactory = await InitializeAsync( + addServices: s => + { + s.AddSingleton(typeof(Guid), + new Guid("00000001-0000-0000-0000-000000000001")); + return s; + }, + usePooling: false); + using var context = contextFactory.CreateContext(); + + var result = context.Set().ToList(); + Assert.Empty(result); + } + + protected class Context38132(DbContextOptions options, Guid tenantId) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity() + .HasQueryFilter(e => e.TenantId == tenantId); + } + + public class Entity38132 + { + public int Id { get; set; } + public string Name { get; set; } + public Guid TenantId { get; set; } + } + + #endregion } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs index d2e44796c43..3cb094d0ede 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs @@ -443,6 +443,20 @@ FROM [Locations] AS [l] ) AS [l0] ON [s].[LocationId] = [l0].[LocationId] WHERE [s].[IsDeleted] = 0 ORDER BY [s].[Name] +"""); + } + + public override async Task Query_filter_with_primary_constructor_parameter() + { + await base.Query_filter_with_primary_constructor_parameter(); + + AssertSql( + """ +@ef_filter__tenantId='00000001-0000-0000-0000-000000000001' + +SELECT [e].[Id], [e].[Name], [e].[TenantId] +FROM [Entity38132] AS [e] +WHERE [e].[TenantId] = @ef_filter__tenantId """); } } From cd01420d9f483e2a74e07338636b856e090b03bd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:18:49 -0700 Subject: [PATCH 5/6] [release/10.0] Fix nullable complex property discriminator changes not persisted (#38134) Stop marking complex type discriminators as PropertySaveBehavior.Throw so they can be properly included in update operations. Set discriminator values when nullable complex properties transition between null and non-null states. Add AppContext quirk for backward compatibility. Fixes #38119 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../ChangeTracking/Internal/ChangeDetector.cs | 18 ++ .../Internal/InternalComplexTypeBuilder.cs | 24 ++ .../Query/AdHocComplexTypeQueryTestBase.cs | 270 +++++++++++++++++- 3 files changed, 307 insertions(+), 5 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 5d2600518a3..d3f23016795 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -346,6 +346,24 @@ public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComple // to ensure the entity is detected as modified and the complex type properties are persisted if (!UseOldBehavior37890 || currentValue is not null) { + if (!InternalComplexTypeBuilder.UseOldBehavior38119) + { + // Set the discriminator value for the complex type when transitioning from null to non-null or vice versa. + // The discriminator is a shadow property whose value needs to be updated to reflect the new state. + var discriminatorProperty = complexProperty.ComplexType.FindDiscriminatorProperty(); + if (discriminatorProperty != null) + { + if (currentValue is not null) + { + entry[discriminatorProperty] = complexProperty.ComplexType.GetDiscriminatorValue(); + } + else if (discriminatorProperty.IsShadowProperty()) + { + entry[discriminatorProperty] = discriminatorProperty.ClrType.GetDefaultValue(); + } + } + } + foreach (var innerProperty in complexProperty.ComplexType.GetFlattenedProperties()) { // Only mark properties that are tracked and can be modified diff --git a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs index aa03bb7c0ff..ff79ac03542 100644 --- a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs @@ -500,6 +500,30 @@ public virtual bool CanSetServiceOnlyConstructorBinding( => configurationSource.Overrides(Metadata.GetServiceOnlyConstructorBindingConfigurationSource()) || Metadata.ServiceOnlyConstructorBinding == constructorBinding; + internal static readonly bool UseOldBehavior38119 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", out var enabled) && enabled; + + /// + /// 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 InternalPropertyBuilder? GetOrCreateDiscriminatorProperty( + Type? type, + string? name, + MemberInfo? memberInfo, + ConfigurationSource configurationSource) + { + var builder = base.GetOrCreateDiscriminatorProperty(type, name, memberInfo, configurationSource); + if (!UseOldBehavior38119) + { + builder?.AfterSave(PropertySaveBehavior.Save, ConfigurationSource.Convention); + } + + return builder; + } + /// /// 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/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index b2123250eac..0ce294f63c2 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -148,13 +148,23 @@ public virtual async Task Optional_complex_type_with_discriminator() return context.SaveChangesAsync(); }); - await using var context = contextFactory.CreateContext(); + await using (var context = contextFactory.CreateContext()) + { + var complexTypeNull = await context.Set() + .SingleAsync(b => b.AllOptionalsComplexType == null); + Assert.Null(complexTypeNull.AllOptionalsComplexType); - var complexTypeNull = await context.Set().SingleAsync(b => b.AllOptionalsComplexType == null); - Assert.Null(complexTypeNull.AllOptionalsComplexType); + complexTypeNull.AllOptionalsComplexType = + new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "New thing" }; + await context.SaveChangesAsync(); + } - complexTypeNull.AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "New thing" }; - await context.SaveChangesAsync(); + await using (var context = contextFactory.CreateContext()) + { + var entities = await context.Set().ToListAsync(); + Assert.Equal(3, entities.Count); + Assert.All(entities, e => Assert.NotNull(e.AllOptionalsComplexType)); + } } private class ContextShadowDiscriminator(DbContextOptions options) : DbContext(options) @@ -401,6 +411,256 @@ public class OptionalComplexProperty #endregion Issue37337 + #region Issue38119 + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add(new Context38119.EntityType { Id = Guid.NewGuid() }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.Null(entity.Prop); + + entity.Prop = new Context38119.OptionalComplexProperty { OptionalValue = true }; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add( + new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + + entity.Prop = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.Null(entity.Prop); + } + } + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add( + new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + + context.Update(entity); + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_set_to_different_value() + { + var contextFactory = await InitializeAsync(); + + Guid entityId; + await using (var context = contextFactory.CreateContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + // Override the discriminator value before saving + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = "SomeOtherValue"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + // The discriminator is non-null so the complex property is still materialized + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_set_to_null() + { + var contextFactory = await InitializeAsync(); + + Guid entityId; + await using (var context = contextFactory.CreateContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + // Set discriminator to null before saving, which should cause the complex property to be null on reload + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + // With null discriminator, the complex property should be materialized as null + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.Null(entity.Prop); + } + } + + [ConditionalFact] + public virtual async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add( + new Context38119Nested.EntityType + { + Id = Guid.NewGuid(), + Outer = new Context38119Nested.OuterComplexProperty { Name = "outer" } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Outer); + Assert.Null(entity.Outer.Inner); + + entity.Outer.Inner = new Context38119Nested.InnerComplexProperty { Value = 42 }; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Outer); + Assert.NotNull(entity.Outer.Inner); + Assert.Equal(42, entity.Outer.Inner.Value); + } + } + + private class Context38119(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.HasKey(p => p.Id); + entity.Property(p => p.Id).ValueGeneratedNever(); + + var compl = entity.ComplexProperty(p => p.Prop); + compl.HasDiscriminator(); + } + + public class EntityType + { + public Guid Id { get; set; } + public OptionalComplexProperty? Prop { get; set; } + } + + public class OptionalComplexProperty + { + public bool? OptionalValue { get; set; } + } + } + + private class Context38119Nested(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.HasKey(p => p.Id); + entity.Property(p => p.Id).ValueGeneratedNever(); + + entity.ComplexProperty( + p => p.Outer, outer => + { + outer.ComplexProperty( + p => p.Inner, inner => inner.HasDiscriminator()); + }); + } + + public class EntityType + { + public Guid Id { get; set; } + public OuterComplexProperty Outer { get; set; } = null!; + } + + public class OuterComplexProperty + { + public string? Name { get; set; } + public InnerComplexProperty? Inner { get; set; } + } + + public class InnerComplexProperty + { + public int? Value { get; set; } + } + } + + #endregion Issue38119 + protected override string StoreName => "AdHocComplexTypeQueryTest"; } From b3077a5fed1e9e79f6e14adf4879961719280a77 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 20 Apr 2026 17:43:17 -0700 Subject: [PATCH 6/6] Remove quirks --- .../Internal/InternalComplexTypeBuilder.cs | 8 +- .../Internal/ExpressionTreeFuncletizer.cs | 2 +- .../Query/AdHocComplexTypeQueryCosmosTest.cs | 114 ++++++++++++++++++ .../Query/AdHocComplexTypeQueryTestBase.cs | 14 +-- .../Query/AdHocQueryFiltersQueryTestBase.cs | 4 +- 5 files changed, 125 insertions(+), 17 deletions(-) diff --git a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs index ff79ac03542..c7e467ed396 100644 --- a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs @@ -500,9 +500,6 @@ public virtual bool CanSetServiceOnlyConstructorBinding( => configurationSource.Overrides(Metadata.GetServiceOnlyConstructorBindingConfigurationSource()) || Metadata.ServiceOnlyConstructorBinding == constructorBinding; - internal static readonly bool UseOldBehavior38119 = - AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", out var enabled) && enabled; - /// /// 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 @@ -516,10 +513,7 @@ public virtual bool CanSetServiceOnlyConstructorBinding( ConfigurationSource configurationSource) { var builder = base.GetOrCreateDiscriminatorProperty(type, name, memberInfo, configurationSource); - if (!UseOldBehavior38119) - { - builder?.AfterSave(PropertySaveBehavior.Save, ConfigurationSource.Convention); - } + builder?.AfterSave(PropertySaveBehavior.Save, ConfigurationSource.Convention); return builder; } diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index 107b1ae720c..f1722590db0 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -2146,7 +2146,7 @@ bool PreserveConvertNode(Expression expression) { parameterName = QueryFilterPrefix + (RemoveConvert(expression) is MemberExpression { Member.Name: var memberName } - ? "__" + (UseOldBehavior38132 ? memberName : SanitizeCompilerGeneratedName(memberName)) + ? "__" + SanitizeCompilerGeneratedName(memberName) : "__p"); isContextAccessor = true; diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs index c27c7efa56b..502e8227722 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs @@ -49,6 +49,11 @@ SELECT VALUE c FROM root c WHERE (c["AllOptionalsComplexType"] = null) OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c """); } @@ -103,6 +108,115 @@ OFFSET 0 LIMIT 2 """); } + public override async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_set_to_different_value() + { + await base.Nullable_complex_type_with_discriminator_set_to_different_value(); + } + + public override async Task Nullable_complex_type_with_discriminator_set_to_null() + { + // On Cosmos, setting the discriminator shadow property to null doesn't affect materialization + // because the complex property's data is still present in the JSON document. + var contextFactory = await InitializeNonSharedTest(); + + Guid entityId; + await using (var context = contextFactory.CreateDbContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + + } + + public override async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + protected override DbContextOptionsBuilder AddNonSharedOptions(DbContextOptionsBuilder builder) => base.AddNonSharedOptions(builder) .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)); diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index bc7ebe95f6c..6f42292c9bc 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -394,7 +394,7 @@ public class OptionalComplexProperty [ConditionalFact] public virtual async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() { - var contextFactory = await InitializeAsync( + var contextFactory = await InitializeNonSharedTest( seed: context => { context.Add(new Context38119.EntityType { Id = Guid.NewGuid() }); @@ -421,7 +421,7 @@ public virtual async Task Nullable_complex_type_with_discriminator_null_to_non_n [ConditionalFact] public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() { - var contextFactory = await InitializeAsync( + var contextFactory = await InitializeNonSharedTest( seed: context => { context.Add( @@ -452,7 +452,7 @@ public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_n [ConditionalFact] public virtual async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() { - var contextFactory = await InitializeAsync( + var contextFactory = await InitializeNonSharedTest( seed: context => { context.Add( @@ -485,7 +485,7 @@ public virtual async Task Nullable_complex_type_with_discriminator_update_non_nu [ConditionalFact] public virtual async Task Nullable_complex_type_with_discriminator_set_to_different_value() { - var contextFactory = await InitializeAsync(); + var contextFactory = await InitializeNonSharedTest(); Guid entityId; await using (var context = contextFactory.CreateDbContext()) @@ -517,7 +517,7 @@ public virtual async Task Nullable_complex_type_with_discriminator_set_to_differ [ConditionalFact] public virtual async Task Nullable_complex_type_with_discriminator_set_to_null() { - var contextFactory = await InitializeAsync(); + var contextFactory = await InitializeNonSharedTest(); Guid entityId; await using (var context = contextFactory.CreateDbContext()) @@ -548,7 +548,7 @@ public virtual async Task Nullable_complex_type_with_discriminator_set_to_null() [ConditionalFact] public virtual async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() { - var contextFactory = await InitializeAsync( + var contextFactory = await InitializeNonSharedTest( seed: context => { context.Add( @@ -579,7 +579,7 @@ public virtual async Task Nested_nullable_complex_type_with_discriminator_null_t } } - private class Context38119(DbContextOptions options) : DbContext(options) + protected class Context38119(DbContextOptions options) : DbContext(options) { protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs index f124e39dc5a..7d0f0f352e0 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs @@ -821,7 +821,7 @@ public class FooBar35111 [ConditionalFact] public virtual async Task Query_filter_with_primary_constructor_parameter() { - var contextFactory = await InitializeAsync( + var contextFactory = await InitializeNonSharedTest( addServices: s => { s.AddSingleton(typeof(Guid), @@ -829,7 +829,7 @@ public virtual async Task Query_filter_with_primary_constructor_parameter() return s; }, usePooling: false); - using var context = contextFactory.CreateContext(); + using var context = contextFactory.CreateDbContext(); var result = context.Set().ToList(); Assert.Empty(result);