diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index c020cc154cd..b4e927adf85 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -2080,13 +2080,7 @@ bool PreserveConvertNode(Expression expression) if (evaluateAsParameter) { - parameterName = tempParameterName ?? "p"; - - var compilerPrefixIndex = parameterName.LastIndexOf('>'); - if (compilerPrefixIndex != -1) - { - parameterName = parameterName[(compilerPrefixIndex + 1)..]; - } + parameterName = string.IsNullOrWhiteSpace(tempParameterName) ? "p" : tempParameterName; // The VB compiler prefixes closure member names with $VB$Local_, remove that (#33150) if (parameterName.StartsWith("$VB$Local_", StringComparison.Ordinal)) @@ -2094,6 +2088,14 @@ bool PreserveConvertNode(Expression expression) parameterName = parameterName.Substring("$VB$Local_".Length); } + // In many databases, parameter names must start with a letter or underscore. + // The same is true for C# variable names, from which we derive the parameter name, so in principle we shouldn't see an issue; + // but just in case, prepend an underscore if the parameter name doesn't start with a letter or underscore. + if (!char.IsLetter(parameterName[0]) && parameterName[0] != '_') + { + parameterName = "_" + parameterName; + } + parameterName = Uniquifier.Uniquify(parameterName, _parameterNames, maxLength: int.MaxValue, uniquifier: _parameterNames.Count); _parameterNames.Add(parameterName); @@ -2147,11 +2149,13 @@ static Expression RemoveConvert(Expression expression) switch (memberExpression.Member) { case FieldInfo fieldInfo: - parameterName = parameterName is null ? fieldInfo.Name : $"{parameterName}_{fieldInfo.Name}"; + var name = SanitizeCompilerGeneratedName(fieldInfo.Name); + parameterName = parameterName is null ? name : $"{parameterName}_{name}"; return fieldInfo.GetValue(instanceValue); case PropertyInfo propertyInfo: - parameterName = parameterName is null ? propertyInfo.Name : $"{parameterName}_{propertyInfo.Name}"; + name = SanitizeCompilerGeneratedName(propertyInfo.Name); + parameterName = parameterName is null ? name : $"{parameterName}_{name}"; return propertyInfo.GetValue(instanceValue); } } @@ -2166,7 +2170,7 @@ static Expression RemoveConvert(Expression expression) return constantExpression.Value; case MethodCallExpression methodCallExpression: - parameterName = methodCallExpression.Method.Name; + parameterName = SanitizeCompilerGeneratedName(methodCallExpression.Method.Name); break; case UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression @@ -2190,6 +2194,25 @@ static Expression RemoveConvert(Expression expression) exception); } } + + static string SanitizeCompilerGeneratedName(string s) + { + // Compiler-generated field names intentionally contain illegal characters, specifically angle brackets <>. + // In cases where there's something within the angle brackets, that tends to be the original user-provided variable name + // (e.g. k__BackingField). If we see angle brackets, extract that out, or it the angle brackets contain no + // content, strip them out entirely and take what comes after. + var closingBracket = s.IndexOf('>'); + if (closingBracket == -1) + { + return s; + } + + var openingBracket = s.IndexOf('<'); + + return openingBracket != -1 && openingBracket < closingBracket - 1 + ? s[(openingBracket + 1)..closingBracket] + : s[(closingBracket + 1)..]; + } } private Expression ConvertIfNeeded(Expression expression, Type type) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index b183992a6e7..9a713bfda47 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -2031,6 +2031,18 @@ FROM root c SELECT VALUE c FROM root c WHERE (c["$type"] = "Order") +"""); + }); + + public override Task Captured_variable_from_switch_case_pattern_matching(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.Captured_variable_from_switch_case_pattern_matching(a); + + AssertSql( + """ +ReadItem(None, ALFKI) """); }); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index 0b70b83c750..bbff0e844c1 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -2993,6 +2993,23 @@ public virtual Task Parameter_extraction_can_throw_exception_from_user_code_2(bo && o.OrderDate.Value.Year == dateFilter.Value.Year))); } + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Captured_variable_from_switch_case_pattern_matching(bool async) + { + object customerIdAsObject = "ALFKI"; + + switch (customerIdAsObject) + { + case string customerId: + { + await AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID == customerId)); + break; + } + } + } + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Subquery_member_pushdown_does_not_change_original_subquery_model(bool async) => AssertQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index 7612cc2ca39..8104a760aa1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -3236,6 +3236,20 @@ FROM [Orders] AS [o] """); } + public override async Task Captured_variable_from_switch_case_pattern_matching(bool async) + { + await base.Captured_variable_from_switch_case_pattern_matching(async); + + AssertSql( + """ +@customerId='ALFKI' (Size = 5) (DbType = StringFixedLength) + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = @customerId +"""); + } + public override async Task Subquery_member_pushdown_does_not_change_original_subquery_model(bool async) { await base.Subquery_member_pushdown_does_not_change_original_subquery_model(async);