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 2f1fa32e1c5..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 @@ -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.IsDiscriminator && p.SerializedName is not null) + .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) || (property.SerializedName is not null && 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..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 @@ -452,5 +452,108 @@ 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))); + + // 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 + // 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))); + + // 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); + } } }