From 4034ebb784e5a86947b63f47364312459f480510 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:22:58 +0000 Subject: [PATCH 01/13] Initial plan From 5df419dc55a6500f31481343782a508f24af2058 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:35:19 +0000 Subject: [PATCH 02/13] Sanitize lifted constant variable names in precompiled query generation + test Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/PrecompiledQueryCodeGenerator.cs | 48 +++++++++++++++++-- ...AdHocPrecompiledQueryRelationalTestBase.cs | 29 +++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index fa488ff6f90..e04490d4daa 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -975,12 +975,52 @@ private void GenerateQueryExecutor( var queryExecutorAfterLiftingExpression = _liftableConstantProcessor.LiftConstants(queryExecutor, materializerLiftableConstantContext, variableNames); - foreach (var liftedConstant in _liftableConstantProcessor.LiftedConstants) + // Lifted constant variable names are partly derived from model metadata (e.g. property names), which may not be valid C# + // identifiers (shadow properties can be given arbitrary names). Sanitize any such names and replace the corresponding + // parameters across all the lifted constant expressions and the query executor. + var liftedConstants = _liftableConstantProcessor.LiftedConstants; + var sanitizedNames = new string[liftedConstants.Count]; + var liftedConstantExpressions = new Expression[liftedConstants.Count]; + List? originalParameters = null; + List? sanitizedParameters = null; + for (var i = 0; i < liftedConstants.Count; i++) + { + var (parameter, expression) = liftedConstants[i]; + liftedConstantExpressions[i] = expression; + + var sanitizedName = SanitizeIdentifierName(parameter.Name ?? "unknown"); + if (sanitizedName != parameter.Name) + { + var baseName = sanitizedName; + for (var j = 0; variableNames.Contains(sanitizedName); j++) + { + sanitizedName = baseName + j; + } + + (originalParameters ??= []).Add(parameter); + (sanitizedParameters ??= []).Add(Expression.Parameter(parameter.Type, sanitizedName)); + } + + variableNames.Add(sanitizedName); + sanitizedNames[i] = sanitizedName; + } + + if (originalParameters is not null) + { + var replacer = new ReplacingExpressionVisitor(originalParameters, sanitizedParameters!); + queryExecutorAfterLiftingExpression = replacer.Visit(queryExecutorAfterLiftingExpression); + + for (var i = 0; i < liftedConstantExpressions.Length; i++) + { + liftedConstantExpressions[i] = replacer.Visit(liftedConstantExpressions[i]); + } + } + + for (var i = 0; i < liftedConstants.Count; i++) { var variableValueSyntax = _linqToCSharpTranslator.TranslateExpression( - liftedConstant.Expression, _constantReplacements, _memberAccessReplacements, namespaces, unsafeAccessors); - // code.AppendLine($"{liftedConstant.Parameter.Type.Name} {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); - code.AppendLine($"var {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); + liftedConstantExpressions[i], _constantReplacements, _memberAccessReplacements, namespaces, unsafeAccessors); + code.AppendLine($"var {sanitizedNames[i]} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); } var queryExecutorSyntaxTree = diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs index 865ab6f8b86..79fba24c6cf 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs @@ -387,6 +387,35 @@ public class InvalidNameNestedEntity public string Name2 { get; set; } = ""; } + [Fact] + public virtual async Task Invalid_identifier_shadow_property_name() + { + var contextFactory = await InitializeNonSharedTest(); + var options = contextFactory.GetOptions(); + + await Test( + """ +await using var context = new AdHocPrecompiledQueryRelationalTestBase.InvalidShadowNameContext(dbContextOptions); +var entities = await context.Entities.ToListAsync(); +""", + typeof(InvalidShadowNameContext), + options); + } + + public class InvalidShadowNameContext(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity() + .Property("NOT VALID !!!1").HasConversion(x => 0, x => ""); + } + + public class InvalidShadowNameEntity + { + public Guid Id { get; set; } + } + #endregion protected TestSqlLoggerFactory TestSqlLoggerFactory From 975fab3f0632abf5f00a1daf1c2d20038f8003ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:50:22 +0000 Subject: [PATCH 03/13] Add warning for shadow property names that are not valid C# identifiers Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Diagnostics/CoreEventId.cs | 15 +++++ .../Diagnostics/CoreLoggerExtensions.cs | 34 ++++++++++ src/EFCore/Diagnostics/LoggingDefinitions.cs | 9 +++ src/EFCore/EFCore.baseline.json | 6 ++ src/EFCore/Infrastructure/ModelValidator.cs | 65 +++++++++++++++++++ src/EFCore/Properties/CoreStrings.Designer.cs | 25 +++++++ src/EFCore/Properties/CoreStrings.resx | 4 ++ ...AdHocPrecompiledQueryRelationalTestBase.cs | 3 +- .../Infrastructure/ModelValidatorTest.cs | 34 ++++++++++ 9 files changed, 194 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Diagnostics/CoreEventId.cs b/src/EFCore/Diagnostics/CoreEventId.cs index e68acbf39c9..0a4e8f45bf9 100644 --- a/src/EFCore/Diagnostics/CoreEventId.cs +++ b/src/EFCore/Diagnostics/CoreEventId.cs @@ -129,6 +129,7 @@ private enum Id NoEntityTypeConfigurationsWarning = CoreBaseId + 632, AccidentalEntityType = CoreBaseId + 633, AccidentalComplexPropertyCollection = CoreBaseId + 634, + ShadowPropertyNameNotValidIdentifierWarning = CoreBaseId + 635, // ChangeTracking events DetectChangesStarting = CoreBaseId + 800, @@ -750,6 +751,20 @@ private static EventId MakeModelValidationId(Id id) /// public static readonly EventId AccidentalComplexPropertyCollection = MakeModelValidationId(Id.AccidentalComplexPropertyCollection); + /// + /// A shadow property has a name that is not a valid C# identifier, which prevents it from being used in precompiled queries. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId ShadowPropertyNameNotValidIdentifierWarning = + MakeModelValidationId(Id.ShadowPropertyNameNotValidIdentifierWarning); + /// /// The on the collection navigation property was ignored. /// diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs index 3b76077e276..79e201f5fe1 100644 --- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs +++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs @@ -1432,6 +1432,40 @@ private static string ShadowPropertyCreated(EventDefinitionBase definition, Even return d.GenerateMessage(p.Property.DeclaringType.DisplayName(), p.Property.Name); } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The property. + public static void ShadowPropertyNameNotValidIdentifierWarning( + this IDiagnosticsLogger diagnostics, + IProperty property) + { + var definition = CoreResources.LogShadowPropertyNameNotValidIdentifier(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, property.DeclaringType.DisplayName(), property.Name); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new PropertyEventData( + definition, + ShadowPropertyNameNotValidIdentifier, + property); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string ShadowPropertyNameNotValidIdentifier(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (PropertyEventData)payload; + return d.GenerateMessage(p.Property.DeclaringType.DisplayName(), p.Property.Name); + } + /// /// Logs for the event. /// diff --git a/src/EFCore/Diagnostics/LoggingDefinitions.cs b/src/EFCore/Diagnostics/LoggingDefinitions.cs index 55133e004e3..d0498c902df 100644 --- a/src/EFCore/Diagnostics/LoggingDefinitions.cs +++ b/src/EFCore/Diagnostics/LoggingDefinitions.cs @@ -825,4 +825,13 @@ public abstract class LoggingDefinitions /// [EntityFrameworkInternal] public EventDefinitionBase? LogQueryCompilationStarting; + + /// + /// 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 EventDefinitionBase? LogShadowPropertyNameNotValidIdentifier; } diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index 7c9812b71e2..23cc7464300 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -3092,6 +3092,9 @@ { "Member": "static readonly Microsoft.Extensions.Logging.EventId ShadowPropertyCreated" }, + { + "Member": "static readonly Microsoft.Extensions.Logging.EventId ShadowPropertyNameNotValidIdentifierWarning" + }, { "Member": "static readonly Microsoft.Extensions.Logging.EventId SkipCollectionChangeDetected" }, @@ -3355,6 +3358,9 @@ { "Member": "static void ShadowPropertyCreated(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, Microsoft.EntityFrameworkCore.Metadata.IProperty property);" }, + { + "Member": "static void ShadowPropertyNameNotValidIdentifierWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, Microsoft.EntityFrameworkCore.Metadata.IProperty property);" + }, { "Member": "static void SkipCollectionChangeDetected(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry internalEntityEntry, Microsoft.EntityFrameworkCore.Metadata.ISkipNavigation navigation, System.Collections.Generic.ISet added, System.Collections.Generic.ISet removed);" }, diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index e0a7e8f743c..eb30c7968b0 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.ObjectModel; +using System.Globalization; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -174,6 +175,70 @@ protected virtual void ValidateProperty( ValidateTypeMapping(property, logger); ValidatePrimitiveCollection(property, logger); ValidateAutoLoaded(property, structuralType, logger); + + if (property.IsShadowProperty() + && !IsValidCSharpIdentifier(property.Name)) + { + logger.ShadowPropertyNameNotValidIdentifierWarning(property); + } + } + + private static bool IsValidCSharpIdentifier(string name) + { + if (string.IsNullOrEmpty(name) + || !IsIdentifierStartCharacter(name[0])) + { + return false; + } + + for (var i = 1; i < name.Length; i++) + { + if (!IsIdentifierPartCharacter(name[i])) + { + return false; + } + } + + return true; + + static bool IsIdentifierStartCharacter(char ch) + => ch < 'a' + ? ch is >= 'A' and (<= 'Z' or '_') + : ch <= 'z' || (ch > '\u007F' && IsLetterChar(CharUnicodeInfo.GetUnicodeCategory(ch))); + + static bool IsIdentifierPartCharacter(char ch) + { + if (ch < 'a') + { + return (ch < 'A' ? ch is >= '0' and <= '9' : ch <= 'Z') || ch == '_'; + } + + if (ch <= 'z') + { + return true; + } + + if (ch <= '\u007F') + { + return false; + } + + var cat = CharUnicodeInfo.GetUnicodeCategory(ch); + return IsLetterChar(cat) + || cat is UnicodeCategory.DecimalDigitNumber + or UnicodeCategory.ConnectorPunctuation + or UnicodeCategory.NonSpacingMark + or UnicodeCategory.SpacingCombiningMark + or UnicodeCategory.Format; + } + + static bool IsLetterChar(UnicodeCategory cat) + => cat is UnicodeCategory.UppercaseLetter + or UnicodeCategory.LowercaseLetter + or UnicodeCategory.TitlecaseLetter + or UnicodeCategory.ModifierLetter + or UnicodeCategory.OtherLetter + or UnicodeCategory.LetterNumber; } /// diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 186af2df532..d9368ed5897 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -5557,6 +5557,31 @@ public static EventDefinition LogShadowPropertyCreated(IDiagnost return (EventDefinition)definition; } + /// + /// The shadow property '{entityType}.{property}' has a name that is not a valid C# identifier. Precompiled queries that use this property will fail to compile. Consider renaming the property to a valid C# identifier. + /// + public static EventDefinition LogShadowPropertyNameNotValidIdentifier(IDiagnosticsLogger logger) + { + var definition = ((LoggingDefinitions)logger.Definitions).LogShadowPropertyNameNotValidIdentifier; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((LoggingDefinitions)logger.Definitions).LogShadowPropertyNameNotValidIdentifier, + logger, + static logger => new EventDefinition( + logger.Options, + CoreEventId.ShadowPropertyNameNotValidIdentifierWarning, + LogLevel.Warning, + "CoreEventId.ShadowPropertyNameNotValidIdentifierWarning", + level => LoggerMessage.Define( + level, + CoreEventId.ShadowPropertyNameNotValidIdentifierWarning, + _resourceManager.GetString("LogShadowPropertyNameNotValidIdentifier")!))); + } + + return (EventDefinition)definition; + } + /// /// {addedCount} entities were added and {removedCount} entities were removed from skip navigation '{entityType}.{property}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see key values. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 09ee563d2d1..13f459c928c 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1230,6 +1230,10 @@ The property '{entityType}.{property}' was created in shadow state because there are no eligible CLR members with a matching name. Debug CoreEventId.ShadowPropertyCreated string string + + The shadow property '{entityType}.{property}' has a name that is not a valid C# identifier. Precompiled queries that use this property will fail to compile. Consider renaming the property to a valid C# identifier. + Warning CoreEventId.ShadowPropertyNameNotValidIdentifierWarning string string + {addedCount} entities were added and {removedCount} entities were removed from skip navigation '{entityType}.{property}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see key values. Debug CoreEventId.SkipCollectionChangeDetected int int string string diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs index 79fba24c6cf..a36e2612871 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocPrecompiledQueryRelationalTestBase.cs @@ -390,7 +390,8 @@ public class InvalidNameNestedEntity [Fact] public virtual async Task Invalid_identifier_shadow_property_name() { - var contextFactory = await InitializeNonSharedTest(); + var contextFactory = await InitializeNonSharedTest( + onConfiguring: o => o.ConfigureWarnings(w => w.Ignore(CoreEventId.ShadowPropertyNameNotValidIdentifierWarning))); var options = contextFactory.GetOptions(); await Test( diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 275a48afaea..a0c3b9bb75f 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -462,6 +462,40 @@ public virtual void Passes_on_shadow_primary_key_created_by_convention_in_depend .GenerateMessage("A", "Key"), modelBuilder, LogLevel.Debug); } + [Fact] + public virtual void Warns_on_shadow_property_name_that_is_not_a_valid_identifier() + { + var modelBuilder = CreateConventionlessModelBuilder(); + var model = modelBuilder.Model; + + var entityType = model.AddEntityType(typeof(A)); + SetPrimaryKey(entityType); + AddProperties(entityType); + + entityType.AddProperty("NOT VALID !!!1", typeof(string)); + + VerifyWarning( + CoreResources.LogShadowPropertyNameNotValidIdentifier(new TestLogger()) + .GenerateMessage("A", "NOT VALID !!!1"), modelBuilder, LogLevel.Warning); + } + + [Fact] + public virtual void Does_not_warn_on_shadow_property_with_valid_identifier_name() + { + var modelBuilder = CreateConventionlessModelBuilder(); + var model = modelBuilder.Model; + + var entityType = model.AddEntityType(typeof(A)); + SetPrimaryKey(entityType); + AddProperties(entityType); + + entityType.AddProperty("ValidName", typeof(string)); + + VerifyLogDoesNotContain( + CoreResources.LogShadowPropertyNameNotValidIdentifier(new TestLogger()) + .GenerateMessage("A", "ValidName"), modelBuilder); + } + [Fact] // Issue #33484 public virtual void Does_not_log_for_shadow_property_when_creating_indexer_property() { From d83e0d597326d61d7de4113c44881d20e13c6122 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:52:02 +0000 Subject: [PATCH 04/13] Document why C# identifier validation is replicated in runtime assembly Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Infrastructure/ModelValidator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index eb30c7968b0..303994f2ce8 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -185,6 +185,8 @@ protected virtual void ValidateProperty( private static bool IsValidCSharpIdentifier(string name) { + // This mirrors the Unicode rules used by Roslyn (and CSharpHelper in the Design assembly), replicated here because the + // runtime assembly does not reference Roslyn. if (string.IsNullOrEmpty(name) || !IsIdentifierStartCharacter(name[0])) { From a928c0d389421a3856fc525b42406588d5a76b2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:29:55 +0000 Subject: [PATCH 05/13] Move sanitization into LiftConstants; simplify identifier check; update warning message Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/PrecompiledQueryCodeGenerator.cs | 48 ++-------------- src/EFCore/Infrastructure/ModelValidator.cs | 55 +++---------------- src/EFCore/Properties/CoreStrings.Designer.cs | 2 +- src/EFCore/Properties/CoreStrings.resx | 2 +- src/EFCore/Query/LiftableConstantProcessor.cs | 6 +- 5 files changed, 20 insertions(+), 93 deletions(-) diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index e04490d4daa..fa488ff6f90 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -975,52 +975,12 @@ private void GenerateQueryExecutor( var queryExecutorAfterLiftingExpression = _liftableConstantProcessor.LiftConstants(queryExecutor, materializerLiftableConstantContext, variableNames); - // Lifted constant variable names are partly derived from model metadata (e.g. property names), which may not be valid C# - // identifiers (shadow properties can be given arbitrary names). Sanitize any such names and replace the corresponding - // parameters across all the lifted constant expressions and the query executor. - var liftedConstants = _liftableConstantProcessor.LiftedConstants; - var sanitizedNames = new string[liftedConstants.Count]; - var liftedConstantExpressions = new Expression[liftedConstants.Count]; - List? originalParameters = null; - List? sanitizedParameters = null; - for (var i = 0; i < liftedConstants.Count; i++) - { - var (parameter, expression) = liftedConstants[i]; - liftedConstantExpressions[i] = expression; - - var sanitizedName = SanitizeIdentifierName(parameter.Name ?? "unknown"); - if (sanitizedName != parameter.Name) - { - var baseName = sanitizedName; - for (var j = 0; variableNames.Contains(sanitizedName); j++) - { - sanitizedName = baseName + j; - } - - (originalParameters ??= []).Add(parameter); - (sanitizedParameters ??= []).Add(Expression.Parameter(parameter.Type, sanitizedName)); - } - - variableNames.Add(sanitizedName); - sanitizedNames[i] = sanitizedName; - } - - if (originalParameters is not null) - { - var replacer = new ReplacingExpressionVisitor(originalParameters, sanitizedParameters!); - queryExecutorAfterLiftingExpression = replacer.Visit(queryExecutorAfterLiftingExpression); - - for (var i = 0; i < liftedConstantExpressions.Length; i++) - { - liftedConstantExpressions[i] = replacer.Visit(liftedConstantExpressions[i]); - } - } - - for (var i = 0; i < liftedConstants.Count; i++) + foreach (var liftedConstant in _liftableConstantProcessor.LiftedConstants) { var variableValueSyntax = _linqToCSharpTranslator.TranslateExpression( - liftedConstantExpressions[i], _constantReplacements, _memberAccessReplacements, namespaces, unsafeAccessors); - code.AppendLine($"var {sanitizedNames[i]} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); + liftedConstant.Expression, _constantReplacements, _memberAccessReplacements, namespaces, unsafeAccessors); + // code.AppendLine($"{liftedConstant.Parameter.Type.Name} {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); + code.AppendLine($"var {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); } var queryExecutorSyntaxTree = diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 303994f2ce8..e70bd4c8a54 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.ObjectModel; -using System.Globalization; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -177,70 +176,34 @@ protected virtual void ValidateProperty( ValidateAutoLoaded(property, structuralType, logger); if (property.IsShadowProperty() - && !IsValidCSharpIdentifier(property.Name)) + && !IsValidIdentifier(property.Name)) { logger.ShadowPropertyNameNotValidIdentifierWarning(property); } } - private static bool IsValidCSharpIdentifier(string name) + /// + /// Returns if the given name only uses letters, digits and underscores and does not start with a digit; + /// that is, if it can be used as-is as a C# identifier in generated code. + /// + internal static bool IsValidIdentifier(string? name) { - // This mirrors the Unicode rules used by Roslyn (and CSharpHelper in the Design assembly), replicated here because the - // runtime assembly does not reference Roslyn. if (string.IsNullOrEmpty(name) - || !IsIdentifierStartCharacter(name[0])) + || (!char.IsLetter(name[0]) && name[0] != '_')) { return false; } for (var i = 1; i < name.Length; i++) { - if (!IsIdentifierPartCharacter(name[i])) + var ch = name[i]; + if (!char.IsLetter(ch) && !char.IsAsciiDigit(ch) && ch != '_') { return false; } } return true; - - static bool IsIdentifierStartCharacter(char ch) - => ch < 'a' - ? ch is >= 'A' and (<= 'Z' or '_') - : ch <= 'z' || (ch > '\u007F' && IsLetterChar(CharUnicodeInfo.GetUnicodeCategory(ch))); - - static bool IsIdentifierPartCharacter(char ch) - { - if (ch < 'a') - { - return (ch < 'A' ? ch is >= '0' and <= '9' : ch <= 'Z') || ch == '_'; - } - - if (ch <= 'z') - { - return true; - } - - if (ch <= '\u007F') - { - return false; - } - - var cat = CharUnicodeInfo.GetUnicodeCategory(ch); - return IsLetterChar(cat) - || cat is UnicodeCategory.DecimalDigitNumber - or UnicodeCategory.ConnectorPunctuation - or UnicodeCategory.NonSpacingMark - or UnicodeCategory.SpacingCombiningMark - or UnicodeCategory.Format; - } - - static bool IsLetterChar(UnicodeCategory cat) - => cat is UnicodeCategory.UppercaseLetter - or UnicodeCategory.LowercaseLetter - or UnicodeCategory.TitlecaseLetter - or UnicodeCategory.ModifierLetter - or UnicodeCategory.OtherLetter - or UnicodeCategory.LetterNumber; } /// diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index d9368ed5897..8c72415e1ec 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -5558,7 +5558,7 @@ public static EventDefinition LogShadowPropertyCreated(IDiagnost } /// - /// The shadow property '{entityType}.{property}' has a name that is not a valid C# identifier. Precompiled queries that use this property will fail to compile. Consider renaming the property to a valid C# identifier. + /// The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property using only numbers, letters and underscore. /// public static EventDefinition LogShadowPropertyNameNotValidIdentifier(IDiagnosticsLogger logger) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 13f459c928c..8ef2bef77eb 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1231,7 +1231,7 @@ Debug CoreEventId.ShadowPropertyCreated string string - The shadow property '{entityType}.{property}' has a name that is not a valid C# identifier. Precompiled queries that use this property will fail to compile. Consider renaming the property to a valid C# identifier. + The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property using only numbers, letters and underscore. Warning CoreEventId.ShadowPropertyNameNotValidIdentifierWarning string string diff --git a/src/EFCore/Query/LiftableConstantProcessor.cs b/src/EFCore/Query/LiftableConstantProcessor.cs index 525d460bb1c..80dd5a61aa8 100644 --- a/src/EFCore/Query/LiftableConstantProcessor.cs +++ b/src/EFCore/Query/LiftableConstantProcessor.cs @@ -126,7 +126,11 @@ public virtual Expression LiftConstants(Expression expression, ParameterExpressi continue; } - var name = liftedConstant.Parameter.Name ?? "unknown"; + // Lifted constant variable names are partly derived from model metadata (e.g. property names), which may not be valid C# + // identifiers (shadow properties can be given arbitrary names). Fall back to a generic name in that case. + var name = ModelValidator.IsValidIdentifier(liftedConstant.Parameter.Name) + ? liftedConstant.Parameter.Name! + : "unknown"; var baseName = name; for (var j = 0; variableNames.Contains(name); j++) { From 0e95a844b9b7d4ffb032a4b4a53d7573c54602ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 04:42:52 +0000 Subject: [PATCH 06/13] Use strict IsValidIdentifier logic in CSharpHelper identifier char checks Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/Internal/CSharpHelper.cs | 68 +------------------ 1 file changed, 2 insertions(+), 66 deletions(-) diff --git a/src/EFCore.Design/Design/Internal/CSharpHelper.cs b/src/EFCore.Design/Design/Internal/CSharpHelper.cs index c62d8194854..953469b0133 100644 --- a/src/EFCore.Design/Design/Internal/CSharpHelper.cs +++ b/src/EFCore.Design/Design/Internal/CSharpHelper.cs @@ -1633,72 +1633,8 @@ public virtual string Expression( } private static bool IsIdentifierStartCharacter(char ch) - { - if (ch < 'a') - { - return ch is >= 'A' and (<= 'Z' or '_'); - } - - if (ch <= 'z') - { - return true; - } - - return ch > '\u007F' && IsLetterChar(CharUnicodeInfo.GetUnicodeCategory(ch)); - } + => char.IsLetter(ch) || ch == '_'; private static bool IsIdentifierPartCharacter(char ch) - { - if (ch < 'a') - { - return (ch < 'A' - ? ch is >= '0' and <= '9' - : ch <= 'Z') - || ch == '_'; - } - - if (ch <= 'z') - { - return true; - } - - if (ch <= '\u007F') - { - return false; - } - - var cat = CharUnicodeInfo.GetUnicodeCategory(ch); - if (IsLetterChar(cat)) - { - return true; - } - - switch (cat) - { - case UnicodeCategory.DecimalDigitNumber: - case UnicodeCategory.ConnectorPunctuation: - case UnicodeCategory.NonSpacingMark: - case UnicodeCategory.SpacingCombiningMark: - case UnicodeCategory.Format: - return true; - } - - return false; - } - - private static bool IsLetterChar(UnicodeCategory cat) - { - switch (cat) - { - case UnicodeCategory.UppercaseLetter: - case UnicodeCategory.LowercaseLetter: - case UnicodeCategory.TitlecaseLetter: - case UnicodeCategory.ModifierLetter: - case UnicodeCategory.OtherLetter: - case UnicodeCategory.LetterNumber: - return true; - } - - return false; - } + => char.IsLetter(ch) || char.IsAsciiDigit(ch) || ch == '_'; } From eeb2c519fb76afa6f8fd03ea45527f1f8071a15f Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 18 Jun 2026 23:27:46 -0700 Subject: [PATCH 07/13] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/EFCore/Diagnostics/CoreEventId.cs | 2 +- src/EFCore/Infrastructure/ModelValidator.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Diagnostics/CoreEventId.cs b/src/EFCore/Diagnostics/CoreEventId.cs index 0a4e8f45bf9..8ec0fc0f7fb 100644 --- a/src/EFCore/Diagnostics/CoreEventId.cs +++ b/src/EFCore/Diagnostics/CoreEventId.cs @@ -752,7 +752,7 @@ private static EventId MakeModelValidationId(Id id) public static readonly EventId AccidentalComplexPropertyCollection = MakeModelValidationId(Id.AccidentalComplexPropertyCollection); /// - /// A shadow property has a name that is not a valid C# identifier, which prevents it from being used in precompiled queries. + /// A shadow property has a name that is not a valid identifier, which can cause issues in generated code. /// /// /// diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index e70bd4c8a54..c3974a1cfc3 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -183,8 +183,8 @@ protected virtual void ValidateProperty( } /// - /// Returns if the given name only uses letters, digits and underscores and does not start with a digit; - /// that is, if it can be used as-is as a C# identifier in generated code. + /// Returns if the given name only uses letters, ASCII digits and underscores and does not start with a digit; + /// that is, if it can be used as-is as an identifier in generated code. /// internal static bool IsValidIdentifier(string? name) { From 1d734842afd181bb8501cf5df5d76c9655648af1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 06:34:02 +0000 Subject: [PATCH 08/13] Adjust invalid shadow property warning message Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Properties/CoreStrings.Designer.cs | 2 +- src/EFCore/Properties/CoreStrings.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 8c72415e1ec..c2c0a1c764e 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -5558,7 +5558,7 @@ public static EventDefinition LogShadowPropertyCreated(IDiagnost } /// - /// The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property using only numbers, letters and underscore. + /// The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits and underscore. /// public static EventDefinition LogShadowPropertyNameNotValidIdentifier(IDiagnosticsLogger logger) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 8ef2bef77eb..e50f1964c7e 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1231,7 +1231,7 @@ Debug CoreEventId.ShadowPropertyCreated string string - The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property using only numbers, letters and underscore. + The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits and underscore. Warning CoreEventId.ShadowPropertyNameNotValidIdentifierWarning string string From 3a006aaae43673eeccd5a15d0698ad485c622378 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 06:34:29 +0000 Subject: [PATCH 09/13] Polish invalid shadow property warning text Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Properties/CoreStrings.Designer.cs | 2 +- src/EFCore/Properties/CoreStrings.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index c2c0a1c764e..1539ef0796a 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -5558,7 +5558,7 @@ public static EventDefinition LogShadowPropertyCreated(IDiagnost } /// - /// The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits and underscore. + /// The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits, and underscore. /// public static EventDefinition LogShadowPropertyNameNotValidIdentifier(IDiagnosticsLogger logger) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index e50f1964c7e..bc24b148df1 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1231,7 +1231,7 @@ Debug CoreEventId.ShadowPropertyCreated string string - The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits and underscore. + The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits, and underscore. Warning CoreEventId.ShadowPropertyNameNotValidIdentifierWarning string string From 9573291693555102960e8aaf70b6d517c4e395c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:21:28 +0000 Subject: [PATCH 10/13] Adjust invalid shadow property warning wording Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Properties/CoreStrings.Designer.cs | 2 +- src/EFCore/Properties/CoreStrings.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 1539ef0796a..b7c29e725f2 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -5558,7 +5558,7 @@ public static EventDefinition LogShadowPropertyCreated(IDiagnost } /// - /// The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits, and underscore. + /// The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to use only letters, digits (except for the first character), and underscore. /// public static EventDefinition LogShadowPropertyNameNotValidIdentifier(IDiagnosticsLogger logger) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index bc24b148df1..0166459de44 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1231,7 +1231,7 @@ Debug CoreEventId.ShadowPropertyCreated string string - The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to start with a letter or underscore, and use only letters, digits, and underscore. + The shadow property '{entityType}.{property}' has a name that is not a valid identifier. This can cause issues in generated code. Consider renaming the property to use only letters, digits (except for the first character), and underscore. Warning CoreEventId.ShadowPropertyNameNotValidIdentifierWarning string string From ee5e1f17c9bdfef9a4a4d901d7f1ded58586c657 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:09:18 +0000 Subject: [PATCH 11/13] Update Cosmos discriminator shadow property name Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../CosmosDiscriminatorConvention.cs | 14 +++++++---- .../Query/NorthwindQueryCosmosFixture.cs | 23 +++++++++++-------- .../Extensions/CosmosBuilderExtensionsTest.cs | 20 ++++++++++++++-- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs index 2990718e5c0..df9507ee2ef 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; @@ -21,6 +22,8 @@ public class CosmosDiscriminatorConvention : IEntityTypeAnnotationChangedConvention, IModelEmbeddedDiscriminatorNameConvention { + private const string DiscriminatorPropertyName = "_type"; + /// /// Creates a new instance of . /// @@ -86,8 +89,9 @@ private static void ProcessEntityType(IConventionEntityTypeBuilder entityTypeBui if (entityType.IsDocumentRoot()) { - entityTypeBuilder.HasDiscriminator(entityType.Model.GetEmbeddedDiscriminatorName(), typeof(string)) - ?.HasValue(entityType, entityType.ShortName()); + var discriminator = entityTypeBuilder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); + discriminator?.EntityType.FindDiscriminatorProperty()?.Builder.ToJsonProperty(entityType.Model.GetEmbeddedDiscriminatorName()); + discriminator?.HasValue(entityType, entityType.ShortName()); } else { @@ -125,7 +129,8 @@ public override void ProcessEntityTypeBaseTypeChanged( { if (entityType.IsDocumentRoot()) { - entityTypeBuilder.HasDiscriminator(entityType.Model.GetEmbeddedDiscriminatorName(), typeof(string)); + var discriminator = entityTypeBuilder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); + discriminator?.EntityType.FindDiscriminatorProperty()?.Builder.ToJsonProperty(entityType.Model.GetEmbeddedDiscriminatorName()); } } else @@ -137,9 +142,10 @@ public override void ProcessEntityTypeBaseTypeChanged( return; } - var discriminator = rootType.Builder.HasDiscriminator(entityType.Model.GetEmbeddedDiscriminatorName(), typeof(string)); + var discriminator = rootType.Builder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); if (discriminator != null) { + discriminator.EntityType.FindDiscriminatorProperty()?.Builder.ToJsonProperty(entityType.Model.GetEmbeddedDiscriminatorName()); SetDefaultDiscriminatorValues(entityTypeBuilder.Metadata.GetDerivedTypesInclusive(), discriminator); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs index fd75389851b..1e8f5bb69b8 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs @@ -52,32 +52,35 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasRootDiscriminatorInJsonId() .ToContainer("ProductsAndOrders"); - modelBuilder.Entity() - .ToContainer("ProductsAndOrders") - .HasRootDiscriminatorInJsonId() - .HasDiscriminator("$type").HasValue("Order"); + modelBuilder.Entity().ToContainer("ProductsAndOrders").HasRootDiscriminatorInJsonId(); + modelBuilder.Entity().HasDiscriminator("_type").HasValue("Order"); + modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); modelBuilder .Entity() .ToContainer("ProductsAndOrders") - .HasRootDiscriminatorInJsonId() - .HasDiscriminator("$type").HasValue("Product"); + .HasRootDiscriminatorInJsonId(); + modelBuilder.Entity().HasDiscriminator("_type").HasValue("Product"); + modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); modelBuilder .Entity() .ToContainer("ProductsAndOrders") - .HasRootDiscriminatorInJsonId() - .HasDiscriminator("$type").HasValue("ProductView"); + .HasRootDiscriminatorInJsonId(); + modelBuilder.Entity().HasDiscriminator("_type").HasValue("ProductView"); + modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); modelBuilder .Entity() .ToContainer("Customers") - .HasDiscriminator("$type").HasValue("Customer"); + .HasDiscriminator("_type").HasValue("Customer"); + modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); modelBuilder .Entity() .ToContainer("Customers") - .HasDiscriminator("$type").HasValue("Customer"); + .HasDiscriminator("_type").HasValue("Customer"); + modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); modelBuilder.Entity().Metadata.RemoveIndex( modelBuilder.Entity().Property(e => e.City).Metadata.GetContainingIndexes().Single()); diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs index 193fe2084af..49945ead2d9 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs @@ -135,7 +135,8 @@ public void Default_discriminator_can_be_removed() var entityType = modelBuilder.Model.FindEntityType(typeof(Customer))!; - Assert.Equal("$type", entityType.FindDiscriminatorProperty()!.Name); + Assert.Equal("_type", entityType.FindDiscriminatorProperty()!.Name); + Assert.Equal("$type", entityType.FindDiscriminatorProperty()!.GetJsonPropertyName()); Assert.Equal(nameof(Customer), entityType.GetDiscriminatorValue()); modelBuilder.Entity().HasNoDiscriminator(); @@ -145,7 +146,8 @@ public void Default_discriminator_can_be_removed() modelBuilder.Entity().HasBaseType(); - Assert.Equal("$type", entityType.FindDiscriminatorProperty()!.Name); + Assert.Equal("_type", entityType.FindDiscriminatorProperty()!.Name); + Assert.Equal("$type", entityType.FindDiscriminatorProperty()!.GetJsonPropertyName()); Assert.Equal(nameof(Customer), entityType.GetDiscriminatorValue()); modelBuilder.Entity().HasBaseType((string)null); @@ -166,6 +168,20 @@ public void Can_set_etag_concurrency_entity() Assert.True(etagProperty.IsConcurrencyToken); } + [Fact] + public void Default_discriminator_property_uses_embedded_discriminator_json_name() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.HasEmbeddedDiscriminatorName("Terminator"); + modelBuilder.Entity(); + + var discriminatorProperty = modelBuilder.Model.FindEntityType(typeof(Customer))!.FindDiscriminatorProperty()!; + + Assert.Equal("_type", discriminatorProperty.Name); + Assert.Equal("Terminator", discriminatorProperty.GetJsonPropertyName()); + } + [Fact] public void Can_set_etag_concurrency_property() { From 955413398d2ad875d4895d7cfd4e7fc0b145ae81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:15:04 +0000 Subject: [PATCH 12/13] Polish Cosmos discriminator configuration updates Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../CosmosDiscriminatorConvention.cs | 25 +++++++++----- .../Query/NorthwindQueryCosmosFixture.cs | 34 +++++++++---------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs index df9507ee2ef..00126127dc4 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; @@ -86,11 +85,9 @@ private static void ProcessEntityType(IConventionEntityTypeBuilder entityTypeBui { return; } - if (entityType.IsDocumentRoot()) { - var discriminator = entityTypeBuilder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); - discriminator?.EntityType.FindDiscriminatorProperty()?.Builder.ToJsonProperty(entityType.Model.GetEmbeddedDiscriminatorName()); + var discriminator = HasDiscriminator(entityTypeBuilder); discriminator?.HasValue(entityType, entityType.ShortName()); } else @@ -129,8 +126,7 @@ public override void ProcessEntityTypeBaseTypeChanged( { if (entityType.IsDocumentRoot()) { - var discriminator = entityTypeBuilder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); - discriminator?.EntityType.FindDiscriminatorProperty()?.Builder.ToJsonProperty(entityType.Model.GetEmbeddedDiscriminatorName()); + HasDiscriminator(entityTypeBuilder); } } else @@ -142,15 +138,28 @@ public override void ProcessEntityTypeBaseTypeChanged( return; } - var discriminator = rootType.Builder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); + var discriminator = HasDiscriminator(rootType.Builder); if (discriminator != null) { - discriminator.EntityType.FindDiscriminatorProperty()?.Builder.ToJsonProperty(entityType.Model.GetEmbeddedDiscriminatorName()); SetDefaultDiscriminatorValues(entityTypeBuilder.Metadata.GetDerivedTypesInclusive(), discriminator); } } } + private static IConventionDiscriminatorBuilder? HasDiscriminator(IConventionEntityTypeBuilder entityTypeBuilder) + { + var discriminator = entityTypeBuilder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); + var discriminatorProperty = discriminator?.EntityType.FindDiscriminatorProperty(); + if (discriminatorProperty != null) + { + CosmosPropertyBuilderExtensions.ToJsonProperty( + discriminatorProperty.Builder, + entityTypeBuilder.Metadata.Model.GetEmbeddedDiscriminatorName()); + } + + return discriminator; + } + /// protected override void SetDefaultDiscriminatorValues( IEnumerable entityTypes, diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs index 1e8f5bb69b8..1d4f647ed78 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs @@ -52,35 +52,35 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasRootDiscriminatorInJsonId() .ToContainer("ProductsAndOrders"); - modelBuilder.Entity().ToContainer("ProductsAndOrders").HasRootDiscriminatorInJsonId(); - modelBuilder.Entity().HasDiscriminator("_type").HasValue("Order"); - modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); + var orderQuery = modelBuilder.Entity().ToContainer("ProductsAndOrders").HasRootDiscriminatorInJsonId(); + orderQuery.HasDiscriminator("_type").HasValue("Order"); + orderQuery.Property("_type").ToJsonProperty("$type"); - modelBuilder + var productQuery = modelBuilder .Entity() .ToContainer("ProductsAndOrders") .HasRootDiscriminatorInJsonId(); - modelBuilder.Entity().HasDiscriminator("_type").HasValue("Product"); - modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); + productQuery.HasDiscriminator("_type").HasValue("Product"); + productQuery.Property("_type").ToJsonProperty("$type"); - modelBuilder + var productView = modelBuilder .Entity() .ToContainer("ProductsAndOrders") .HasRootDiscriminatorInJsonId(); - modelBuilder.Entity().HasDiscriminator("_type").HasValue("ProductView"); - modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); + productView.HasDiscriminator("_type").HasValue("ProductView"); + productView.Property("_type").ToJsonProperty("$type"); - modelBuilder + var customerQueryWithQueryFilter = modelBuilder .Entity() - .ToContainer("Customers") - .HasDiscriminator("_type").HasValue("Customer"); - modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); + .ToContainer("Customers"); + customerQueryWithQueryFilter.HasDiscriminator("_type").HasValue("Customer"); + customerQueryWithQueryFilter.Property("_type").ToJsonProperty("$type"); - modelBuilder + var customerQuery = modelBuilder .Entity() - .ToContainer("Customers") - .HasDiscriminator("_type").HasValue("Customer"); - modelBuilder.Entity().Property("_type").ToJsonProperty("$type"); + .ToContainer("Customers"); + customerQuery.HasDiscriminator("_type").HasValue("Customer"); + customerQuery.Property("_type").ToJsonProperty("$type"); modelBuilder.Entity().Metadata.RemoveIndex( modelBuilder.Entity().Property(e => e.City).Metadata.GetContainingIndexes().Single()); From c6bb2875e46272a6fa1849053fd3de764f8b7485 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 04:46:15 +0000 Subject: [PATCH 13/13] Use default Cosmos discriminator property name Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../CosmosDiscriminatorConvention.cs | 4 +--- .../Query/NorthwindQueryCosmosFixture.cs | 20 +++++++++---------- .../Extensions/CosmosBuilderExtensionsTest.cs | 6 +++--- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs index 00126127dc4..1e007b7aaf5 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosDiscriminatorConvention.cs @@ -21,8 +21,6 @@ public class CosmosDiscriminatorConvention : IEntityTypeAnnotationChangedConvention, IModelEmbeddedDiscriminatorNameConvention { - private const string DiscriminatorPropertyName = "_type"; - /// /// Creates a new instance of . /// @@ -148,7 +146,7 @@ public override void ProcessEntityTypeBaseTypeChanged( private static IConventionDiscriminatorBuilder? HasDiscriminator(IConventionEntityTypeBuilder entityTypeBuilder) { - var discriminator = entityTypeBuilder.HasDiscriminator(DiscriminatorPropertyName, typeof(string)); + var discriminator = entityTypeBuilder.HasDiscriminator(typeof(string)); var discriminatorProperty = discriminator?.EntityType.FindDiscriminatorProperty(); if (discriminatorProperty != null) { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs index 1d4f647ed78..dae02c001a8 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs @@ -53,34 +53,34 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .ToContainer("ProductsAndOrders"); var orderQuery = modelBuilder.Entity().ToContainer("ProductsAndOrders").HasRootDiscriminatorInJsonId(); - orderQuery.HasDiscriminator("_type").HasValue("Order"); - orderQuery.Property("_type").ToJsonProperty("$type"); + orderQuery.HasDiscriminator().HasValue("Order"); + orderQuery.Property("Discriminator").ToJsonProperty("$type"); var productQuery = modelBuilder .Entity() .ToContainer("ProductsAndOrders") .HasRootDiscriminatorInJsonId(); - productQuery.HasDiscriminator("_type").HasValue("Product"); - productQuery.Property("_type").ToJsonProperty("$type"); + productQuery.HasDiscriminator().HasValue("Product"); + productQuery.Property("Discriminator").ToJsonProperty("$type"); var productView = modelBuilder .Entity() .ToContainer("ProductsAndOrders") .HasRootDiscriminatorInJsonId(); - productView.HasDiscriminator("_type").HasValue("ProductView"); - productView.Property("_type").ToJsonProperty("$type"); + productView.HasDiscriminator().HasValue("ProductView"); + productView.Property("Discriminator").ToJsonProperty("$type"); var customerQueryWithQueryFilter = modelBuilder .Entity() .ToContainer("Customers"); - customerQueryWithQueryFilter.HasDiscriminator("_type").HasValue("Customer"); - customerQueryWithQueryFilter.Property("_type").ToJsonProperty("$type"); + customerQueryWithQueryFilter.HasDiscriminator().HasValue("Customer"); + customerQueryWithQueryFilter.Property("Discriminator").ToJsonProperty("$type"); var customerQuery = modelBuilder .Entity() .ToContainer("Customers"); - customerQuery.HasDiscriminator("_type").HasValue("Customer"); - customerQuery.Property("_type").ToJsonProperty("$type"); + customerQuery.HasDiscriminator().HasValue("Customer"); + customerQuery.Property("Discriminator").ToJsonProperty("$type"); modelBuilder.Entity().Metadata.RemoveIndex( modelBuilder.Entity().Property(e => e.City).Metadata.GetContainingIndexes().Single()); diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs index 49945ead2d9..91a680b2538 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosBuilderExtensionsTest.cs @@ -135,7 +135,7 @@ public void Default_discriminator_can_be_removed() var entityType = modelBuilder.Model.FindEntityType(typeof(Customer))!; - Assert.Equal("_type", entityType.FindDiscriminatorProperty()!.Name); + Assert.Equal("Discriminator", entityType.FindDiscriminatorProperty()!.Name); Assert.Equal("$type", entityType.FindDiscriminatorProperty()!.GetJsonPropertyName()); Assert.Equal(nameof(Customer), entityType.GetDiscriminatorValue()); @@ -146,7 +146,7 @@ public void Default_discriminator_can_be_removed() modelBuilder.Entity().HasBaseType(); - Assert.Equal("_type", entityType.FindDiscriminatorProperty()!.Name); + Assert.Equal("Discriminator", entityType.FindDiscriminatorProperty()!.Name); Assert.Equal("$type", entityType.FindDiscriminatorProperty()!.GetJsonPropertyName()); Assert.Equal(nameof(Customer), entityType.GetDiscriminatorValue()); @@ -178,7 +178,7 @@ public void Default_discriminator_property_uses_embedded_discriminator_json_name var discriminatorProperty = modelBuilder.Model.FindEntityType(typeof(Customer))!.FindDiscriminatorProperty()!; - Assert.Equal("_type", discriminatorProperty.Name); + Assert.Equal("Discriminator", discriminatorProperty.Name); Assert.Equal("Terminator", discriminatorProperty.GetJsonPropertyName()); }