From a6cb704d48f52b6805a623fa2dfe9460a3bde76b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:57:02 +0000 Subject: [PATCH 1/5] Initial plan From 6bf6dd5fef5dd5e57c79c64556fb28a102548204 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:04:08 +0000 Subject: [PATCH 2/5] Fix duplicate discriminator property in derived types by checking serialized names When a derived model has a discriminator property with a different C# name but the same serialized name as the base model's discriminator, the property was incorrectly being generated in the derived class, causing CS0108 "hides inherited member" warnings. The fix adds a check for serialized names in addition to C# property names when determining if a discriminator property should be skipped in derived types. Co-authored-by: JonathanCrd <17486462+JonathanCrd@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 12 ++- .../ModelProviders/DiscriminatorTests.cs | 85 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 37f85d828bf..eb22bf2446d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -423,12 +423,22 @@ protected internal override PropertyProvider[] BuildProperties() var propertiesCount = _inputModel.Properties.Count; var properties = new List(propertiesCount + 1); Dictionary baseProperties = EnumerateBaseModels().SelectMany(m => m.Properties).GroupBy(x => x.Name).Select(g => g.First()).ToDictionary(p => p.Name) ?? []; + // Build a set of serialized names for base discriminator properties to handle cases where + // the derived model has a discriminator with a different C# name but the same wire name + HashSet baseDiscriminatorSerializedNames = EnumerateBaseModels() + .SelectMany(m => m.Properties) + .Where(p => p is InputModelProperty modelProperty && modelProperty.IsDiscriminator) + .Select(p => p.SerializedName) + .ToHashSet(); for (int i = 0; i < propertiesCount; i++) { var property = _inputModel.Properties[i]; var isDiscriminator = IsDiscriminator(property); - if (isDiscriminator && baseProperties.ContainsKey(property.Name)) + // Skip discriminator properties that already exist in the base class + // Check both by C# property name and by serialized name to handle cases where + // the derived model has a discriminator with a different C# name but the same wire name + if (isDiscriminator && (baseProperties.ContainsKey(property.Name) || baseDiscriminatorSerializedNames.Contains(property.SerializedName))) { continue; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs index 0dcde039873..fb94f6d079f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs @@ -452,5 +452,90 @@ public async Task CanCustomizeDiscriminator() Assert.IsNotNull(discriminatorParam); Assert.IsTrue(discriminatorParam!.Property?.IsDiscriminator); } + + // This test validates that a derived model does not re-declare the discriminator property + // when the base model has a discriminator property with a different C# name but the same serialized name. + // This scenario can occur when the base model has a discriminator property named "OdataType" + // and the derived model has a discriminator property also named "OdataType" with the same + // serialized name "@odata.type". + [Test] + public void DerivedDoesNotDuplicateDiscriminatorWithSameSerializedName() + { + // Base model with discriminator named "OdataType" with serialized name "@odata.type" + var baseSkillModel = InputFactory.Model( + "SearchIndexerSkill", + properties: [InputFactory.Property("OdataType", InputPrimitiveType.String, isRequired: true, isDiscriminator: true, serializedName: "@odata.type")], + discriminatedModels: new Dictionary()); + + // Derived model with discriminator named "OdataType" with same serialized name "@odata.type" + var derivedSkillModel = InputFactory.Model( + "ContentUnderstandingSkill", + discriminatedKind: "#Microsoft.Skills.Util.ContentUnderstandingSkill", + baseModel: baseSkillModel, + properties: + [ + InputFactory.Property("OdataType", InputPrimitiveType.String, isRequired: true, isDiscriminator: true, serializedName: "@odata.type"), + InputFactory.Property("Inputs", InputPrimitiveType.String, isRequired: true) + ]); + + MockHelpers.LoadMockGenerator(inputModelTypes: [baseSkillModel, derivedSkillModel]); + var outputLibrary = CodeModelGenerator.Instance.OutputLibrary; + + var derivedModel = outputLibrary.TypeProviders.OfType().FirstOrDefault(t => t.Name == "ContentUnderstandingSkill"); + Assert.IsNotNull(derivedModel); + + // The derived model should NOT have the OdataType property since it's already defined in the base class + var odataTypeProperty = derivedModel!.Properties.FirstOrDefault(p => p.Name == "OdataType"); + Assert.IsNull(odataTypeProperty, "Derived model should not re-declare the discriminator property that exists in the base class"); + + // The public constructor should correctly pass the discriminator value to the base class + var publicCtor = derivedModel.Constructors.FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + Assert.IsNotNull(publicCtor); + // The public ctor should not have an odataType parameter + Assert.IsFalse(publicCtor!.Signature.Parameters.Any(p => p.Name.Equals("odataType", System.StringComparison.OrdinalIgnoreCase))); + } + + // This test validates that a derived model does not re-declare the discriminator property + // when the base model has a discriminator property with a different C# name but the same serialized name. + // This scenario can occur when the base model has a discriminator property named "Type" + // and the derived model has a discriminator property named "OdataType" with the same + // serialized name "@odata.type". + [Test] + public void DerivedDoesNotDuplicateDiscriminatorWithDifferentNameButSameSerializedName() + { + // Base model with discriminator named "Type" with serialized name "@odata.type" + var baseSkillModel = InputFactory.Model( + "SearchIndexerSkill", + properties: [InputFactory.Property("Type", InputPrimitiveType.String, isRequired: true, isDiscriminator: true, serializedName: "@odata.type")], + discriminatedModels: new Dictionary()); + + // Derived model with discriminator named "OdataType" with same serialized name "@odata.type" + var derivedSkillModel = InputFactory.Model( + "ContentUnderstandingSkill", + discriminatedKind: "#Microsoft.Skills.Util.ContentUnderstandingSkill", + baseModel: baseSkillModel, + properties: + [ + InputFactory.Property("OdataType", InputPrimitiveType.String, isRequired: true, isDiscriminator: true, serializedName: "@odata.type"), + InputFactory.Property("Inputs", InputPrimitiveType.String, isRequired: true) + ]); + + MockHelpers.LoadMockGenerator(inputModelTypes: [baseSkillModel, derivedSkillModel]); + var outputLibrary = CodeModelGenerator.Instance.OutputLibrary; + + var derivedModel = outputLibrary.TypeProviders.OfType().FirstOrDefault(t => t.Name == "ContentUnderstandingSkill"); + Assert.IsNotNull(derivedModel); + + // The derived model should NOT have the OdataType property since it's the same discriminator + // (same serialized name) as the base class "Type" property + var odataTypeProperty = derivedModel!.Properties.FirstOrDefault(p => p.Name == "OdataType"); + Assert.IsNull(odataTypeProperty, "Derived model should not re-declare the discriminator property when base has same serialized name"); + + // The public constructor should correctly pass the discriminator value to the base class + var publicCtor = derivedModel.Constructors.FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + Assert.IsNotNull(publicCtor); + // The public ctor should not have an odataType parameter + Assert.IsFalse(publicCtor!.Signature.Parameters.Any(p => p.Name.Equals("odataType", System.StringComparison.OrdinalIgnoreCase))); + } } } From b4caf3236adce6fbc0f8613e05362ff5fbc29216 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:08:41 +0000 Subject: [PATCH 3/5] Address code review: simplify redundant type check in discriminator filtering Co-authored-by: JonathanCrd <17486462+JonathanCrd@users.noreply.github.com> --- .../Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index eb22bf2446d..e557049fff9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -427,7 +427,7 @@ protected internal override PropertyProvider[] BuildProperties() // the derived model has a discriminator with a different C# name but the same wire name HashSet baseDiscriminatorSerializedNames = EnumerateBaseModels() .SelectMany(m => m.Properties) - .Where(p => p is InputModelProperty modelProperty && modelProperty.IsDiscriminator) + .Where(p => p.IsDiscriminator) .Select(p => p.SerializedName) .ToHashSet(); for (int i = 0; i < propertiesCount; i++) From b297323a4c1db3005349bf66cee08eae671859f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:33:10 +0000 Subject: [PATCH 4/5] Add null checks for SerializedName in discriminator property filtering Co-authored-by: JonathanCrd <17486462+JonathanCrd@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index d335296928e..c5a37eb71c9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -427,7 +427,7 @@ protected internal override PropertyProvider[] BuildProperties() // the derived model has a discriminator with a different C# name but the same wire name HashSet baseDiscriminatorSerializedNames = EnumerateBaseModels() .SelectMany(m => m.Properties) - .Where(p => p.IsDiscriminator) + .Where(p => p.IsDiscriminator && p.SerializedName is not null) .Select(p => p.SerializedName) .ToHashSet(); for (int i = 0; i < propertiesCount; i++) @@ -438,7 +438,7 @@ protected internal override PropertyProvider[] BuildProperties() // Skip discriminator properties that already exist in the base class // Check both by C# property name and by serialized name to handle cases where // the derived model has a discriminator with a different C# name but the same wire name - if (isDiscriminator && (baseProperties.ContainsKey(property.Name) || baseDiscriminatorSerializedNames.Contains(property.SerializedName))) + if (isDiscriminator && (baseProperties.ContainsKey(property.Name) || (property.SerializedName is not null && baseDiscriminatorSerializedNames.Contains(property.SerializedName)))) { continue; } From e0d790d4f13cda4768f1f21c99ac0db4b21586d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:29:52 +0000 Subject: [PATCH 5/5] Add verification of correct discriminator value passed to base class in tests Co-authored-by: JonathanCrd <17486462+JonathanCrd@users.noreply.github.com> --- .../ModelProviders/DiscriminatorTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs index fb94f6d079f..a3666593986 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/DiscriminatorTests.cs @@ -493,6 +493,15 @@ public void DerivedDoesNotDuplicateDiscriminatorWithSameSerializedName() Assert.IsNotNull(publicCtor); // The public ctor should not have an odataType parameter Assert.IsFalse(publicCtor!.Signature.Parameters.Any(p => p.Name.Equals("odataType", System.StringComparison.OrdinalIgnoreCase))); + + // Verify the correct discriminator value is passed to the base class + var init = publicCtor.Signature.Initializer; + Assert.IsNotNull(init); + var expression = init!.Arguments[0] as ScopedApi; + Assert.IsNotNull(expression); + var original = expression!.Original as LiteralExpression; + Assert.IsNotNull(original); + Assert.AreEqual("#Microsoft.Skills.Util.ContentUnderstandingSkill", original!.Literal); } // This test validates that a derived model does not re-declare the discriminator property @@ -536,6 +545,15 @@ public void DerivedDoesNotDuplicateDiscriminatorWithDifferentNameButSameSerializ Assert.IsNotNull(publicCtor); // The public ctor should not have an odataType parameter Assert.IsFalse(publicCtor!.Signature.Parameters.Any(p => p.Name.Equals("odataType", System.StringComparison.OrdinalIgnoreCase))); + + // Verify the correct discriminator value is passed to the base class + var init = publicCtor.Signature.Initializer; + Assert.IsNotNull(init); + var expression = init!.Arguments[0] as ScopedApi; + Assert.IsNotNull(expression); + var original = expression!.Original as LiteralExpression; + Assert.IsNotNull(original); + Assert.AreEqual("#Microsoft.Skills.Util.ContentUnderstandingSkill", original!.Literal); } } }