From 4d6d569373a2e3f84ade967c758e5cd5fb035def Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:03:55 +0000 Subject: [PATCH 1/4] Initial plan From 984d556a8c1c889201e286d3c5d010670b03b10f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:07:30 +0000 Subject: [PATCH 2/4] fix: prevent LastContractPropertiesMap from overriding collection property type when element type changes Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/6f54fa17-1102-4fb6-8ce6-812c730e37c3 Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 3 +- .../ModelProviders/ModelProviderTests.cs | 45 +++++++++++++++++++ .../MockInputModel.cs | 10 +++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs 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 c7c8949dda4..ddeeb3e56b4 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 @@ -540,7 +540,8 @@ protected internal override PropertyProvider[] BuildProperties() { if (LastContractPropertiesMap.TryGetValue(outputProperty.Name, out CSharpType? lastContractPropertyType) && - !outputProperty.Type.Equals(lastContractPropertyType)) + !outputProperty.Type.Equals(lastContractPropertyType) && + outputProperty.Type.Arguments.SequenceEqual(lastContractPropertyType.Arguments)) { outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(property); CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract."); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 7f6ef7ca815..286239e1663 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1062,6 +1062,51 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(moreItemsProperty!.Type.Equals(new CSharpType(typeof(IReadOnlyDictionary<,>), typeof(string), elementEnumProvider.Type))); } + [Test] + public async Task BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges() + { + // Simulate the scenario where the element type of a collection property has changed + // (e.g., from Record[] to BulkVMConfiguration[] via @typeChangedFrom). + // The last contract has IList> but the new code model + // produces IList. The override should NOT apply because the element + // type has changed. + var newElementModel = InputFactory.Model( + "NewElementModel", + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String) + ]); + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("items", InputFactory.Array(newElementModel)), + InputFactory.Property("moreItems", InputFactory.Dictionary(newElementModel)) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel, newElementModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + var newElementModelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "NewElementModel") as ModelProvider; + Assert.IsNotNull(newElementModelProvider); + + // The items property should use the new element type (IList), not be + // overridden to the old type (IList>) from last contract + var itemsProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Items"); + Assert.IsNotNull(itemsProperty); + Assert.IsTrue(itemsProperty!.Type.Equals(new CSharpType(typeof(IList<>), newElementModelProvider!.Type))); + + // The moreItems property should use the new element type (IDictionary), not be + // overridden to the old type (IDictionary>) from last contract + var moreItemsProperty = modelProvider.Properties.FirstOrDefault(p => p.Name == "MoreItems"); + Assert.IsNotNull(moreItemsProperty); + Assert.IsTrue(moreItemsProperty!.Type.Equals(new CSharpType(typeof(IDictionary<,>), typeof(string), newElementModelProvider.Type))); + } + [Test] public async Task BackCompat_InternalTypesAreIgnored() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs new file mode 100644 index 00000000000..9ab0d2614ef --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_CollectionPropertyTypeNotOverriddenWhenElementTypeChanges/MockInputModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class MockInputModel + { + public IList> Items { get; } + public IDictionary> MoreItems { get; } + } +} From 1ee98668bb59146463821ee1d0485aff0e9b5ed4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:40:46 +0000 Subject: [PATCH 3/4] add comment explaining Arguments.SequenceEqual usage for list and dictionary coverage Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/f0f7bda5-5e09-47cc-83fe-700c60a15190 Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 3 +++ 1 file changed, 3 insertions(+) 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 ddeeb3e56b4..6410f46fc42 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 @@ -538,6 +538,9 @@ protected internal override PropertyProvider[] BuildProperties() // Targeted backcompat fix for the case where properties were previously generated as read-only collections if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary) { + // We compare Arguments (not just ElementType) to cover both list element types and + // dictionary key/value types. This ensures we only override the collection wrapper + // (e.g. IReadOnlyList → IList) and not when the element type itself has changed. if (LastContractPropertiesMap.TryGetValue(outputProperty.Name, out CSharpType? lastContractPropertyType) && !outputProperty.Type.Equals(lastContractPropertyType) && From e5061a8b05e9199233c0b65b8485c34d6105dee4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:46:33 +0000 Subject: [PATCH 4/4] fix: use AreNamesEqual for argument comparison to handle TypeProvider vs compiled assembly types Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/fa5a9394-f6b4-4432-aa3f-c3ffd8551f7c Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 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 6410f46fc42..83412b4f19d 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 @@ -538,13 +538,17 @@ protected internal override PropertyProvider[] BuildProperties() // Targeted backcompat fix for the case where properties were previously generated as read-only collections if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary) { - // We compare Arguments (not just ElementType) to cover both list element types and - // dictionary key/value types. This ensures we only override the collection wrapper + // We compare Arguments by name (not just ElementType) to cover both list element types + // and dictionary key/value types. This ensures we only override the collection wrapper // (e.g. IReadOnlyList → IList) and not when the element type itself has changed. + // We use AreNamesEqual rather than Equals because the argument types may come from + // different sources (TypeProvider vs compiled assembly) but represent the same logical type. if (LastContractPropertiesMap.TryGetValue(outputProperty.Name, out CSharpType? lastContractPropertyType) && !outputProperty.Type.Equals(lastContractPropertyType) && - outputProperty.Type.Arguments.SequenceEqual(lastContractPropertyType.Arguments)) + outputProperty.Type.Arguments.Count == lastContractPropertyType.Arguments.Count && + outputProperty.Type.Arguments.Zip(lastContractPropertyType.Arguments).All( + pair => pair.First.AreNamesEqual(pair.Second))) { outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(property); CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract.");