From e193e96c856c26f4fed3339992474c805826595a Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 17 Apr 2026 21:03:49 +0200 Subject: [PATCH 1/2] [release/10.0] Fix query filter parameter names with primary constructor parameters 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 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../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..5d05f622502 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_pk_ctor_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..117fb35d0b5 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_pk_ctor_parameter() + { + await base.Query_filter_with_pk_ctor_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 b5609a642eddf9d85d3abd6c09ec85a4cb846590 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 18 Apr 2026 08:18:56 +0200 Subject: [PATCH 2/2] Rename test to Query_filter_with_primary_constructor_parameter Address review feedback: avoid ambiguity with 'pk' (primary key). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Query/AdHocQueryFiltersQueryTestBase.cs | 2 +- .../Query/AdHocQueryFiltersQuerySqlServerTest.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs index 5d05f622502..a82b0b5d86b 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs @@ -819,7 +819,7 @@ public class FooBar35111 #region 38132 [ConditionalFact] - public virtual async Task Query_filter_with_pk_ctor_parameter() + public virtual async Task Query_filter_with_primary_constructor_parameter() { var contextFactory = await InitializeAsync( addServices: s => diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs index 117fb35d0b5..3cb094d0ede 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocQueryFiltersQuerySqlServerTest.cs @@ -446,9 +446,9 @@ ORDER BY [s].[Name] """); } - public override async Task Query_filter_with_pk_ctor_parameter() + public override async Task Query_filter_with_primary_constructor_parameter() { - await base.Query_filter_with_pk_ctor_parameter(); + await base.Query_filter_with_primary_constructor_parameter(); AssertSql( """