From 3b535180762aba661b60d6c2381e9ff46f0b60f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:09:02 +0000 Subject: [PATCH 01/11] Initial plan From fe60e75cbb74550744254556fb85ffa58b2f51f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:25:38 +0000 Subject: [PATCH 02/11] Restore missing last-contract buildable attributes in MRW context Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 75 +++++++++++++++++++ ...ModelReaderWriterContextDefinitionTests.cs | 74 ++++++++++++++++++ .../SampleContext.cs | 16 ++++ .../SampleContext.cs | 26 +++++++ .../SampleContext.cs | 21 ++++++ 5 files changed, 212 insertions(+) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Custom)/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index e21290f420d..8626fa69977 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -95,10 +95,85 @@ protected override IReadOnlyList BuildAttributes() obsoleteTypeJustification); } + // Back-compat: restore any ModelReaderWriterBuildableAttribute that was present in the last contract + // but is missing from the freshly generated set, without introducing duplicates. + AddLastContractBuildableAttributes(attributes, customizedBuildableTypes); + // Sort by the simple type name (last part after the last dot) instead of the fully qualified name return attributes.OrderBy(a => GetSimpleTypeName(a.Key)).Select(kvp => kvp.Value).ToList(); } + /// + /// Restores entries that were declared on the + /// context type in the last contract but are no longer produced by the current generation. Removing a + /// previously-published buildable entry is a source-breaking change for consumers that rely on the + /// context to build those types, so any missing entry is re-added. Entries already produced by the + /// current generation (or supplied by customized code) are left untouched to avoid duplicates. + /// + private void AddLastContractBuildableAttributes( + Dictionary attributes, + HashSet customizedBuildableTypes) + { + if (LastContractView?.Attributes is not { Count: > 0 } lastContractAttributes) + { + return; + } + + // Buildable attributes from the last contract can render the target type with an empty namespace + // (for example "global::.SampleModel") when the referenced type is not defined in the last-contract + // compilation, so dedupe by the simple type name to reliably detect entries that already exist. + var presentSimpleNames = new HashSet(StringComparer.Ordinal); + foreach (var key in attributes.Keys) + { + presentSimpleNames.Add(GetSimpleTypeName(key)); + } + foreach (var customizedType in customizedBuildableTypes) + { + presentSimpleNames.Add(GetSimpleTypeName(customizedType)); + } + + foreach (var attribute in lastContractAttributes) + { + if (!string.Equals( + attribute.Type.FullyQualifiedName, + typeof(ModelReaderWriterBuildableAttribute).FullName, + StringComparison.Ordinal)) + { + continue; + } + + var targetType = GetBuildableAttributeTargetType(attribute); + if (targetType is null) + { + continue; + } + + var identity = GetTypeIdentity(targetType); + var simpleName = GetSimpleTypeName(identity); + + // Only add the entry when neither the current generation nor customized code already produced it. + if (!presentSimpleNames.Add(simpleName)) + { + continue; + } + + attributes[identity] = attribute; + } + } + + private static CSharpType? GetBuildableAttributeTargetType(AttributeStatement attribute) + { + foreach (var argument in attribute.Arguments) + { + if (argument is TypeOfExpression typeOf) + { + return typeOf.Type; + } + } + + return null; + } + private static bool IsBuildableAttribute(MethodBodyStatement statement) { var attribute = statement switch diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 5c562d371ef..89d61866b80 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -1840,6 +1840,80 @@ public async Task CustomizedBuildableAttributesAreNotRegenerated() "Buildable attributes supplied by a customized context should not be regenerated"); } + [Test] + public async Task LastContractBuildableAttributesAreRestoredWhenMissing() + { + // The last contract declared buildable attributes for both RegularModel and RemovedModel, but only + // RegularModel is produced by the current generation. RemovedModel must be restored for back-compat + // and RegularModel must not be duplicated. + var regularModel = InputFactory.Model("RegularModel", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [regularModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + var regularModelCount = buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")); + var removedModelCount = buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModel")); + + Assert.AreEqual(1, regularModelCount, + "RegularModel is produced by the current generation and must not be duplicated by the last contract entry"); + Assert.AreEqual(1, removedModelCount, + "RemovedModel was declared in the last contract and must be restored for back-compat"); + } + + [Test] + public async Task BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes() + { + // RegularModel is produced by the current generation, CustomModel is supplied by customized code, and + // the last contract declares buildable attributes for RegularModel, CustomModel, and RemovedModel. + // Only RemovedModel is missing, so it must be restored, while the entries already produced by the + // generation or customized code must not be duplicated. + var regularModel = InputFactory.Model("RegularModel", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [regularModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + var regularModelCount = buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")); + var customModelCount = buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("CustomModel")); + var removedModelCount = buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModel")); + + Assert.AreEqual(1, regularModelCount, + "RegularModel is produced by the current generation and must appear exactly once"); + Assert.AreEqual(0, customModelCount, + "CustomModel is supplied by customized code and must not be regenerated from the last contract"); + Assert.AreEqual(1, removedModelCount, + "RemovedModel was declared in the last contract and must be restored for back-compat"); + } + + // Buildable attributes restored from the last contract are symbol-based (IsFrameworkType == false), so + // match by fully qualified name to cover both generated and restored entries. + private static List GetBuildableAttributes(ModelReaderWriterContextDefinition contextDefinition) + => contextDefinition.Attributes + .Where(a => string.Equals( + a.Type.FullyQualifiedName, + typeof(ModelReaderWriterBuildableAttribute).FullName, + StringComparison.Ordinal)) + .ToList(); + [Test] public async Task CustomProjectionPropertiesDoNotAddBuildableTypes() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Custom)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Custom)/SampleContext.cs new file mode 100644 index 00000000000..d08a220f253 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Custom)/SampleContext.cs @@ -0,0 +1,16 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.CustomModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class CustomModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs new file mode 100644 index 00000000000..6d00d8b37be --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs @@ -0,0 +1,26 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.CustomModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class RegularModel + { + } + + public partial class CustomModel + { + } + + public partial class RemovedModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs new file mode 100644 index 00000000000..f754f58cf60 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs @@ -0,0 +1,21 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class RegularModel + { + } + + public partial class RemovedModel + { + } +} From d348a4d29165a29edcc97333023804713cb674f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:51:45 +0000 Subject: [PATCH 03/11] Address PR feedback: remove comments, add test with all three attribute layers, validate via TestData Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 10 ---- ...ModelReaderWriterContextDefinitionTests.cs | 53 +++++++++++++++++++ ...ustomAndLastContractBuildableAttributes.cs | 15 ++++++ .../SampleContext.cs | 16 ++++++ .../SampleContext.cs | 36 +++++++++++++ ...RestoredLastContractBuildableAttributes.cs | 17 ++++++ ...ildableAttributesAreRestoredWhenMissing.cs | 15 ++++++ 7 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Custom)/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 8626fa69977..348a246da66 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -103,13 +103,6 @@ protected override IReadOnlyList BuildAttributes() return attributes.OrderBy(a => GetSimpleTypeName(a.Key)).Select(kvp => kvp.Value).ToList(); } - /// - /// Restores entries that were declared on the - /// context type in the last contract but are no longer produced by the current generation. Removing a - /// previously-published buildable entry is a source-breaking change for consumers that rely on the - /// context to build those types, so any missing entry is re-added. Entries already produced by the - /// current generation (or supplied by customized code) are left untouched to avoid duplicates. - /// private void AddLastContractBuildableAttributes( Dictionary attributes, HashSet customizedBuildableTypes) @@ -119,9 +112,6 @@ private void AddLastContractBuildableAttributes( return; } - // Buildable attributes from the last contract can render the target type with an empty namespace - // (for example "global::.SampleModel") when the referenced type is not defined in the last-contract - // compilation, so dedupe by the simple type name to reliably detect entries that already exist. var presentSimpleNames = new HashSet(StringComparer.Ordinal); foreach (var key in attributes.Keys) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 89d61866b80..2f4efbd9bc6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -1867,6 +1867,10 @@ await MockHelpers.LoadMockGeneratorAsync( "RegularModel is produced by the current generation and must not be duplicated by the last contract entry"); Assert.AreEqual(1, removedModelCount, "RemovedModel was declared in the last contract and must be restored for back-compat"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } [Test] @@ -1902,6 +1906,55 @@ await MockHelpers.LoadMockGeneratorAsync( "CustomModel is supplied by customized code and must not be regenerated from the last contract"); Assert.AreEqual(1, removedModelCount, "RemovedModel was declared in the last contract and must be restored for back-compat"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes() + { + // GeneratedModelA and GeneratedModelB are produced by the current generation, CustomModel is supplied + // by customized code, and the last contract additionally declares RemovedModelA and RemovedModelB. The + // generated entries are emitted, the customized entry is left to the customized code, and both removed + // entries are restored for back-compat. + var generatedModelA = InputFactory.Model("GeneratedModelA", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + var generatedModelB = InputFactory.Model("GeneratedModelB", properties: + [ + InputFactory.Property("Property2", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [generatedModelA, generatedModelB], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("GeneratedModelA")), + "GeneratedModelA is produced by the current generation and must appear exactly once"); + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("GeneratedModelB")), + "GeneratedModelB is produced by the current generation and must appear exactly once"); + Assert.AreEqual(0, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("CustomModel")), + "CustomModel is supplied by customized code and must not be regenerated from the last contract"); + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModelA")), + "RemovedModelA was declared in the last contract and must be restored for back-compat"); + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModelB")), + "RemovedModelB was declared in the last contract and must be restored for back-compat"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } // Buildable attributes restored from the last contract are symbol-based (IsFrameworkType == false), so diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs new file mode 100644 index 00000000000..3b2300bdeed --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs @@ -0,0 +1,15 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Custom)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Custom)/SampleContext.cs new file mode 100644 index 00000000000..d08a220f253 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Custom)/SampleContext.cs @@ -0,0 +1,16 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.CustomModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class CustomModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs new file mode 100644 index 00000000000..35cf2c52b7b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs @@ -0,0 +1,36 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.GeneratedModelA))] + [ModelReaderWriterBuildable(typeof(Sample.Models.GeneratedModelB))] + [ModelReaderWriterBuildable(typeof(Sample.Models.CustomModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModelA))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModelB))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class GeneratedModelA + { + } + + public partial class GeneratedModelB + { + } + + public partial class CustomModel + { + } + + public partial class RemovedModelA + { + } + + public partial class RemovedModelB + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs new file mode 100644 index 00000000000..a8ddac9abeb --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs @@ -0,0 +1,17 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.GeneratedModelA))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.GeneratedModelB))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModelA))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModelB))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs new file mode 100644 index 00000000000..3b2300bdeed --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs @@ -0,0 +1,15 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} From d28db69869a8f158276f6f53ae6c05d86cfd5eca Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Fri, 24 Jul 2026 13:21:49 -0500 Subject: [PATCH 04/11] refactor back compat handling --- .../ModelReaderWriterContextDefinition.cs | 18 ++- ...ModelReaderWriterContextDefinitionTests.cs | 150 ++++++++++++++++-- .../SampleContext.cs | 5 + ...ustomAndLastContractBuildableAttributes.cs | 2 +- .../SampleContext.cs | 10 ++ ...RestoredLastContractBuildableAttributes.cs | 4 +- ...ildableAttributesAreRestoredWhenMissing.cs | 2 +- .../SampleContext.cs | 5 + .../SampleContext.cs | 8 + ...esForReferencedAssemblyTypesAreRestored.cs | 16 ++ .../SampleContext.cs | 18 +++ .../SampleContext.cs | 8 + ...utesForTypesNotInAssemblyAreNotRestored.cs | 14 ++ .../SampleContext.cs | 21 +++ ...ProvidersAreNotRestoredFromLastContract.cs | 13 ++ .../SampleContext.cs | 18 +++ 16 files changed, 291 insertions(+), 21 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored(Custom)/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored(Custom)/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract/SampleContext.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 348a246da66..069a25b2613 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -117,11 +117,18 @@ private void AddLastContractBuildableAttributes( { presentSimpleNames.Add(GetSimpleTypeName(key)); } + foreach (var customizedType in customizedBuildableTypes) { presentSimpleNames.Add(GetSimpleTypeName(customizedType)); } + var outputLibraryProviders = new Dictionary(StringComparer.Ordinal); + foreach (var provider in ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders) + { + outputLibraryProviders.TryAdd(GetTypeIdentity(provider.Type), provider); + } + foreach (var attribute in lastContractAttributes) { if (!string.Equals( @@ -139,6 +146,15 @@ private void AddLastContractBuildableAttributes( } var identity = GetTypeIdentity(targetType); + var existsInGeneratedAssembly = outputLibraryProviders.TryGetValue(identity, out var matchingProvider) + ? ShouldWriteProvider(matchingProvider) + : ScmCodeModelGenerator.Instance.SourceInputModel.Customization?.GetTypeByMetadataName(identity) is not null; + + if (!existsInGeneratedAssembly) + { + continue; + } + var simpleName = GetSimpleTypeName(identity); // Only add the entry when neither the current generation nor customized code already produced it. @@ -147,7 +163,7 @@ private void AddLastContractBuildableAttributes( continue; } - attributes[identity] = attribute; + attributes.TryAdd(identity, attribute); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 2f4efbd9bc6..11bc50962cc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -150,6 +150,37 @@ public void RemovedProvidersDoNotContributeBuildableAttributes() } } + [Test] + public async Task RemovedProvidersAreNotRestoredFromLastContract() + { + // A provider can exist in the output library but be pruned by the reference map + // (ShouldWriteProvider == false), so it is never emitted. A last-contract buildable attribute for + // such a type must not be restored, otherwise the context would reference typeof() and + // break compilation. + var keptProvider = new TestMrwSerialization(implementsPersistableModel: true, includeDepModelProperty: false); + var removedProvider = new RemovedProviderWithFrameworkDependency(); + var outputLibrary = new TestOutputLibrary([keptProvider, removedProvider]); + var mockGenerator = MockHelpers.LoadMockGenerator(createOutputLibrary: () => outputLibrary); + mockGenerator.SetupProperty( + p => p.SourceInputModel, + new SourceInputModel(null, await Helpers.GetCompilationFromDirectoryAsync())); + + try + { + CodeModelGenerator.Instance.AddTypeToKeep(keptProvider); + ProviderReferenceMapAnalyzer.Analyze(ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + finally + { + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + } + } + [Test] public async Task VisitorAttributesArePreservedAfterReferenceMapAnalysis() { @@ -1843,9 +1874,11 @@ public async Task CustomizedBuildableAttributesAreNotRegenerated() [Test] public async Task LastContractBuildableAttributesAreRestoredWhenMissing() { - // The last contract declared buildable attributes for both RegularModel and RemovedModel, but only - // RegularModel is produced by the current generation. RemovedModel must be restored for back-compat - // and RegularModel must not be duplicated. + // The last contract declared buildable attributes for RegularModel, RestoredType, and RemovedModel. + // RegularModel is emitted by the current generation. RestoredType still exists in the output library + // but is not itself emitted as a buildable attribute (an enum here), so it must be restored for + // back-compat. RemovedModel no longer exists in the output library and must not be restored, since + // restoring it would emit typeof() and break compilation. var regularModel = InputFactory.Model("RegularModel", properties: [ InputFactory.Property("Property1", InputPrimitiveType.String) @@ -1853,6 +1886,7 @@ public async Task LastContractBuildableAttributesAreRestoredWhenMissing() await MockHelpers.LoadMockGeneratorAsync( inputModels: () => [regularModel], + inputEnums: () => [InputFactory.Int32Enum("RestoredType", [("Value1", 1)])], lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); var contextDefinition = new ModelReaderWriterContextDefinition(); @@ -1860,13 +1894,17 @@ await MockHelpers.LoadMockGeneratorAsync( var regularModelCount = buildableAttributes .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")); + var restoredTypeCount = buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RestoredType")); var removedModelCount = buildableAttributes .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModel")); Assert.AreEqual(1, regularModelCount, "RegularModel is produced by the current generation and must not be duplicated by the last contract entry"); - Assert.AreEqual(1, removedModelCount, - "RemovedModel was declared in the last contract and must be restored for back-compat"); + Assert.AreEqual(1, restoredTypeCount, + "RestoredType is still part of the output library and must be restored for back-compat"); + Assert.AreEqual(0, removedModelCount, + "RemovedModel is no longer part of the output library and must not be restored"); var writer = new TypeProviderWriter(contextDefinition); var file = writer.Write(); @@ -1877,9 +1915,10 @@ await MockHelpers.LoadMockGeneratorAsync( public async Task BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes() { // RegularModel is produced by the current generation, CustomModel is supplied by customized code, and - // the last contract declares buildable attributes for RegularModel, CustomModel, and RemovedModel. - // Only RemovedModel is missing, so it must be restored, while the entries already produced by the - // generation or customized code must not be duplicated. + // the last contract declares buildable attributes for RegularModel, CustomModel, RestoredType, and + // RemovedModel. RestoredType is still in the output library but not emitted as a buildable attribute, + // so it is restored. RemovedModel is no longer in the output library and must not be restored, while + // the entries already produced by the generation or customized code must not be duplicated. var regularModel = InputFactory.Model("RegularModel", properties: [ InputFactory.Property("Property1", InputPrimitiveType.String) @@ -1887,6 +1926,7 @@ public async Task BuildAttributesForBackCompatibilityDeduplicatesAcrossGenerated await MockHelpers.LoadMockGeneratorAsync( inputModels: () => [regularModel], + inputEnums: () => [InputFactory.Int32Enum("RestoredType", [("Value1", 1)])], compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); @@ -1897,6 +1937,8 @@ await MockHelpers.LoadMockGeneratorAsync( .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")); var customModelCount = buildableAttributes .Count(a => a.Arguments.First().ToDisplayString().Contains("CustomModel")); + var restoredTypeCount = buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RestoredType")); var removedModelCount = buildableAttributes .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModel")); @@ -1904,8 +1946,10 @@ await MockHelpers.LoadMockGeneratorAsync( "RegularModel is produced by the current generation and must appear exactly once"); Assert.AreEqual(0, customModelCount, "CustomModel is supplied by customized code and must not be regenerated from the last contract"); - Assert.AreEqual(1, removedModelCount, - "RemovedModel was declared in the last contract and must be restored for back-compat"); + Assert.AreEqual(1, restoredTypeCount, + "RestoredType is still part of the output library and must be restored for back-compat"); + Assert.AreEqual(0, removedModelCount, + "RemovedModel is no longer part of the output library and must not be restored"); var writer = new TypeProviderWriter(contextDefinition); var file = writer.Write(); @@ -1916,9 +1960,10 @@ await MockHelpers.LoadMockGeneratorAsync( public async Task BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes() { // GeneratedModelA and GeneratedModelB are produced by the current generation, CustomModel is supplied - // by customized code, and the last contract additionally declares RemovedModelA and RemovedModelB. The - // generated entries are emitted, the customized entry is left to the customized code, and both removed - // entries are restored for back-compat. + // by customized code, and the last contract additionally declares RestoredTypeA, RestoredTypeB, + // RemovedModelA, and RemovedModelB. The generated entries are emitted, the customized entry is left to + // the customized code, the restored types are still in the output library and are restored, and the + // removed types are no longer in the output library and must not be restored. var generatedModelA = InputFactory.Model("GeneratedModelA", properties: [ InputFactory.Property("Property1", InputPrimitiveType.String) @@ -1930,6 +1975,11 @@ public async Task BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndR await MockHelpers.LoadMockGeneratorAsync( inputModels: () => [generatedModelA, generatedModelB], + inputEnums: () => + [ + InputFactory.Int32Enum("RestoredTypeA", [("Value1", 1)]), + InputFactory.Int32Enum("RestoredTypeB", [("Value1", 1)]) + ], compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); @@ -1946,11 +1996,79 @@ await MockHelpers.LoadMockGeneratorAsync( .Count(a => a.Arguments.First().ToDisplayString().Contains("CustomModel")), "CustomModel is supplied by customized code and must not be regenerated from the last contract"); Assert.AreEqual(1, buildableAttributes - .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModelA")), - "RemovedModelA was declared in the last contract and must be restored for back-compat"); + .Count(a => a.Arguments.First().ToDisplayString().Contains("RestoredTypeA")), + "RestoredTypeA is still part of the output library and must be restored for back-compat"); Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RestoredTypeB")), + "RestoredTypeB is still part of the output library and must be restored for back-compat"); + Assert.AreEqual(0, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModelA")), + "RemovedModelA is no longer part of the output library and must not be restored"); + Assert.AreEqual(0, buildableAttributes .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModelB")), - "RemovedModelB was declared in the last contract and must be restored for back-compat"); + "RemovedModelB is no longer part of the output library and must not be restored"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored() + { + // The last contract declared a buildable attribute for BinaryData, which lives in a referenced + // assembly rather than the generated output. Because the type still resolves through the generated + // code's references, its attribute must be restored for back-compat + var regularModel = InputFactory.Model("RegularModel", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [regularModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")), + "RegularModel is produced by the current generation and must appear exactly once"); + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("BinaryData")), + "BinaryData lives in a referenced assembly and must be restored for back-compat"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored() + { + // The last contract declared a buildable attribute for RemovedModel, which is not produced by the + // current generation and does not resolve through the generated code's references. It must not be + // restored, since emitting typeof() would break compilation. + var regularModel = InputFactory.Model("RegularModel", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [regularModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")), + "RegularModel is produced by the current generation and must appear exactly once"); + Assert.AreEqual(0, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RemovedModel")), + "RemovedModel is not part of the generated assembly and must not be restored"); var writer = new TypeProviderWriter(contextDefinition); var file = writer.Write(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs index 6d00d8b37be..0d9da3b87e8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes(Last)/SampleContext.cs @@ -4,6 +4,7 @@ namespace Sample { [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] [ModelReaderWriterBuildable(typeof(Sample.Models.CustomModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RestoredType))] [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModel))] public partial class SampleContext { @@ -20,6 +21,10 @@ public partial class CustomModel { } + public enum RestoredType + { + } + public partial class RemovedModel { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs index 3b2300bdeed..b98dd8f5e4b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityDeduplicatesAcrossGeneratedCustomAndLastContractBuildableAttributes.cs @@ -8,7 +8,7 @@ namespace Sample { [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] - [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModel))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RestoredType))] public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs index 35cf2c52b7b..a3b804b6fd1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes(Last)/SampleContext.cs @@ -5,6 +5,8 @@ namespace Sample [ModelReaderWriterBuildable(typeof(Sample.Models.GeneratedModelA))] [ModelReaderWriterBuildable(typeof(Sample.Models.GeneratedModelB))] [ModelReaderWriterBuildable(typeof(Sample.Models.CustomModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RestoredTypeA))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RestoredTypeB))] [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModelA))] [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModelB))] public partial class SampleContext @@ -26,6 +28,14 @@ public partial class CustomModel { } + public enum RestoredTypeA + { + } + + public enum RestoredTypeB + { + } + public partial class RemovedModelA { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs index a8ddac9abeb..27c67df96f5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/BuildAttributesForBackCompatibilityIncludesGeneratedCustomAndRestoredLastContractBuildableAttributes.cs @@ -9,8 +9,8 @@ namespace Sample { [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.GeneratedModelA))] [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.GeneratedModelB))] - [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModelA))] - [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModelB))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RestoredTypeA))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RestoredTypeB))] public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs index 3b2300bdeed..b98dd8f5e4b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing.cs @@ -8,7 +8,7 @@ namespace Sample { [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] - [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RemovedModel))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RestoredType))] public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs index f754f58cf60..73f9e3333f6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesAreRestoredWhenMissing/SampleContext.cs @@ -3,6 +3,7 @@ namespace Sample { [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RestoredType))] [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModel))] public partial class SampleContext { @@ -15,6 +16,10 @@ public partial class RegularModel { } + public enum RestoredType + { + } + public partial class RemovedModel { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored(Custom)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored(Custom)/SampleContext.cs new file mode 100644 index 00000000000..355e0b94b10 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored(Custom)/SampleContext.cs @@ -0,0 +1,8 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class SampleContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored.cs new file mode 100644 index 00000000000..e5fda83fdd8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored.cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::System.BinaryData))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored/SampleContext.cs new file mode 100644 index 00000000000..017736ee106 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForReferencedAssemblyTypesAreRestored/SampleContext.cs @@ -0,0 +1,18 @@ +using System; +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(System.BinaryData))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class RegularModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored(Custom)/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored(Custom)/SampleContext.cs new file mode 100644 index 00000000000..355e0b94b10 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored(Custom)/SampleContext.cs @@ -0,0 +1,8 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class SampleContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored.cs new file mode 100644 index 00000000000..e560fba19ea --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored.cs @@ -0,0 +1,14 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored/SampleContext.cs new file mode 100644 index 00000000000..f754f58cf60 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForTypesNotInAssemblyAreNotRestored/SampleContext.cs @@ -0,0 +1,21 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.RemovedModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class RegularModel + { + } + + public partial class RemovedModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract.cs new file mode 100644 index 00000000000..29d6675052c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; + +namespace Sample +{ + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.TestMrwSerialization))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract/SampleContext.cs new file mode 100644 index 00000000000..8ad09cdcbb8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/RemovedProvidersAreNotRestoredFromLastContract/SampleContext.cs @@ -0,0 +1,18 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.TestMrwSerialization))] + [ModelReaderWriterBuildable(typeof(Sample.RemovedProviderWithFrameworkDependency))] + public partial class SampleContext + { + } + + public partial class TestMrwSerialization + { + } + + internal partial class RemovedProviderWithFrameworkDependency + { + } +} From 69c737e9415afb3f94ec73ef72e6c31428c1772f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:34:02 +0000 Subject: [PATCH 05/11] Fix AddLastContractBuildableAttributes: full-identity dedup, metadata-name fix, suppression path Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 069a25b2613..9084f6457f2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -112,17 +112,6 @@ private void AddLastContractBuildableAttributes( return; } - var presentSimpleNames = new HashSet(StringComparer.Ordinal); - foreach (var key in attributes.Keys) - { - presentSimpleNames.Add(GetSimpleTypeName(key)); - } - - foreach (var customizedType in customizedBuildableTypes) - { - presentSimpleNames.Add(GetSimpleTypeName(customizedType)); - } - var outputLibraryProviders = new Dictionary(StringComparer.Ordinal); foreach (var provider in ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders) { @@ -146,24 +135,69 @@ private void AddLastContractBuildableAttributes( } var identity = GetTypeIdentity(targetType); - var existsInGeneratedAssembly = outputLibraryProviders.TryGetValue(identity, out var matchingProvider) - ? ShouldWriteProvider(matchingProvider) - : ScmCodeModelGenerator.Instance.SourceInputModel.Customization?.GetTypeByMetadataName(identity) is not null; - if (!existsInGeneratedAssembly) + // Resolve the TypeProvider for this last-contract target. First check the output library; + // if not found there, fall back to the customization and referenced-assembly layer using + // Namespace+Name to avoid the CLR-metadata-name conversion issues that arise when passing + // a source-style identity string (e.g. "NS.Type") to GetTypeByMetadataName directly. + bool isOutputLibraryType; + TypeProvider? resolvedProvider; + if (outputLibraryProviders.TryGetValue(identity, out var outputLibraryProvider)) { - continue; + if (!ShouldWriteProvider(outputLibraryProvider)) + { + continue; + } + resolvedProvider = outputLibraryProvider; + isOutputLibraryType = true; } + else + { + resolvedProvider = ScmCodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( + targetType.Namespace, + targetType.Name, + targetType.DeclaringType?.Name, + includeReferencedAssemblies: true); - var simpleName = GetSimpleTypeName(identity); + if (resolvedProvider is null) + { + continue; + } + isOutputLibraryType = false; + } - // Only add the entry when neither the current generation nor customized code already produced it. - if (!presentSimpleNames.Add(simpleName)) + // Deduplicate by full type identity across generated, customized, and last-contract entries. + var typeKey = resolvedProvider.Type.FullyQualifiedName; + if (attributes.ContainsKey(typeKey) || customizedBuildableTypes.Contains(identity)) { continue; } - attributes.TryAdd(identity, attribute); + var attributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute)); + var newAttributeStatement = new AttributeStatement(attributeType, TypeOf(resolvedProvider.Type)); + + if (isOutputLibraryType) + { + // For output-library types, reconstruct through the suppression-handling path so that + // [Experimental] and [Obsolete] diagnostics are properly suppressed, consistent with + // how generated attributes are emitted. + string experimentalTypeJustification = $"{resolvedProvider.Type} is experimental and may change in future versions."; + string obsoleteTypeJustification = $"{resolvedProvider.Type} is obsolete and may be removed in future versions."; + AddAttributeForType( + attributes, + newAttributeStatement, + resolvedProvider, + experimentalTypeJustification, + obsoleteTypeJustification); + } + else + { + // For types resolved from the customization layer or referenced assemblies, + // add the attribute directly; their symbol model may not be fully representable + // through the generator's expression tree (e.g. BCL types), so skip the + // CanonicalView-based suppression path that is designed for output-library types. + attributes.Add(typeKey, newAttributeStatement); + } } } From 56f743397f4832139cfcfd452b212c2e4c3c6071 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:50:26 +0000 Subject: [PATCH 06/11] Fix CLR metadata name and dedup identity in AddLastContractBuildableAttributes - Replace source-style FindForTypeInCustomization call with CLR metadata names: add GetClrSimpleMetadataName (name with arity suffix, e.g. Type`1) and GetClrDeclaringTypeChain ('+'-chain for nested types, e.g. Outer`1+Middle), mirroring NamedTypeSymbolProvider.GetMetadataName so that GetTypeByMetadataName correctly resolves generic and nested types. - Use resolvedProvider's normalized type identity (GetTypeIdentity) for the customized-buildable dedup check instead of the last-contract target identity, ensuring both dedup paths use the same key source. Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 9084f6457f2..d6c242058c3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -136,10 +136,6 @@ private void AddLastContractBuildableAttributes( var identity = GetTypeIdentity(targetType); - // Resolve the TypeProvider for this last-contract target. First check the output library; - // if not found there, fall back to the customization and referenced-assembly layer using - // Namespace+Name to avoid the CLR-metadata-name conversion issues that arise when passing - // a source-style identity string (e.g. "NS.Type") to GetTypeByMetadataName directly. bool isOutputLibraryType; TypeProvider? resolvedProvider; if (outputLibraryProviders.TryGetValue(identity, out var outputLibraryProvider)) @@ -153,10 +149,19 @@ private void AddLastContractBuildableAttributes( } else { + // Build the CLR metadata name parts (arity suffix for generics, '+'-chain for + // nested types), mirroring NamedTypeSymbolProvider.GetMetadataName, so that + // GetTypeByMetadataName resolves generic (e.g. "Type`1") and nested + // (e.g. "Outer+Inner") types correctly. + var clrSimpleName = GetClrSimpleMetadataName(targetType); + var clrDeclaringChain = targetType.DeclaringType is null + ? null + : GetClrDeclaringTypeChain(targetType.DeclaringType); + resolvedProvider = ScmCodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( targetType.Namespace, - targetType.Name, - targetType.DeclaringType?.Name, + clrSimpleName, + clrDeclaringChain, includeReferencedAssemblies: true); if (resolvedProvider is null) @@ -166,9 +171,11 @@ private void AddLastContractBuildableAttributes( isOutputLibraryType = false; } - // Deduplicate by full type identity across generated, customized, and last-contract entries. + // Deduplicate using the resolved provider's normalized type identity so that both the + // attributes dictionary check and the customized-buildable check use the same key source. var typeKey = resolvedProvider.Type.FullyQualifiedName; - if (attributes.ContainsKey(typeKey) || customizedBuildableTypes.Contains(identity)) + var resolvedIdentity = GetTypeIdentity(resolvedProvider.Type); + if (attributes.ContainsKey(typeKey) || customizedBuildableTypes.Contains(resolvedIdentity)) { continue; } @@ -259,6 +266,21 @@ private static string GetTypeIdentity(CSharpType type) : $"{name}<{string.Join(",", type.Arguments.Select(GetTypeIdentity))}>"; } + // Returns the CLR metadata simple name with arity suffix for generic types (e.g. "Type`1"), + // mirroring NamedTypeSymbolProvider.GetMetadataName which uses symbol.MetadataName. + private static string GetClrSimpleMetadataName(CSharpType type) + => type.Arguments.Count > 0 ? $"{type.Name}`{type.Arguments.Count}" : type.Name; + + // Returns the full CLR declaring-type chain using '+' separators (e.g. "Outer`1+Middle"), + // mirroring the recursive NamedTypeSymbolProvider.GetMetadataName pattern. + private static string GetClrDeclaringTypeChain(CSharpType type) + { + var simpleName = GetClrSimpleMetadataName(type); + return type.DeclaringType is null + ? simpleName + : $"{GetClrDeclaringTypeChain(type.DeclaringType)}+{simpleName}"; + } + /// /// Collects all types that implement IPersistableModel, including all models and their properties /// that are also IPersistableModel types, recursively without duplicates. From ec66c37cd46198096445bb8bcb7e27102dcfd075 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:46:47 +0000 Subject: [PATCH 07/11] Refactor: add s_buildableAttributeType field, remove string params from AddAttributeForType, expose GetClrMetadataName on CSharpType Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 69 +++++-------------- .../src/Primitives/CSharpType.cs | 14 ++++ 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index d6c242058c3..341a31268e5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -20,6 +20,7 @@ public class ModelReaderWriterContextDefinition : TypeProvider { private const string DefaultObsoleteDiagnosticId = "CS0618"; private const string ExperimentalAttributeFullName = "System.Diagnostics.CodeAnalysis.ExperimentalAttribute"; + private static readonly CSharpType s_buildableAttributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute)); private static readonly CSharpTypeNameComparer s_cSharpTypeNameComparer = new CSharpTypeNameComparer(); private static readonly TypeProviderTypeNameComparer s_typeProviderNameComparer = new TypeProviderTypeNameComparer(); @@ -58,9 +59,7 @@ protected override IReadOnlyList BuildAttributes() continue; } - // Use the full attribute type name to ensure proper compilation - var attributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute)); - var attributeStatement = new AttributeStatement(attributeType, TypeOf(type)); + var attributeStatement = new AttributeStatement(s_buildableAttributeType, TypeOf(type)); string experimentalTypeJustification = $"{type} is experimental and may change in future versions."; string obsoleteTypeJustification = $"{type} is obsolete and may be removed in future versions."; @@ -79,20 +78,13 @@ protected override IReadOnlyList BuildAttributes() continue; } - // Use the full attribute type name to ensure proper compilation - var attributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute)); - var attributeStatement = new AttributeStatement(attributeType, TypeOf(provider.Type)); - - string experimentalTypeJustification = $"{provider.Type} is experimental and may change in future versions."; - string obsoleteTypeJustification = $"{provider.Type} is obsolete and may be removed in future versions."; + var attributeStatement = new AttributeStatement(s_buildableAttributeType, TypeOf(provider.Type)); // If the type is experimental or obsolete, we add a suppression for it AddAttributeForType( attributes, attributeStatement, - provider, - experimentalTypeJustification, - obsoleteTypeJustification); + provider); } // Back-compat: restore any ModelReaderWriterBuildableAttribute that was present in the last contract @@ -122,7 +114,7 @@ private void AddLastContractBuildableAttributes( { if (!string.Equals( attribute.Type.FullyQualifiedName, - typeof(ModelReaderWriterBuildableAttribute).FullName, + s_buildableAttributeType.FullyQualifiedName, StringComparison.Ordinal)) { continue; @@ -149,19 +141,10 @@ private void AddLastContractBuildableAttributes( } else { - // Build the CLR metadata name parts (arity suffix for generics, '+'-chain for - // nested types), mirroring NamedTypeSymbolProvider.GetMetadataName, so that - // GetTypeByMetadataName resolves generic (e.g. "Type`1") and nested - // (e.g. "Outer+Inner") types correctly. - var clrSimpleName = GetClrSimpleMetadataName(targetType); - var clrDeclaringChain = targetType.DeclaringType is null - ? null - : GetClrDeclaringTypeChain(targetType.DeclaringType); - resolvedProvider = ScmCodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( targetType.Namespace, - clrSimpleName, - clrDeclaringChain, + targetType.GetClrMetadataName(), + null, includeReferencedAssemblies: true); if (resolvedProvider is null) @@ -180,22 +163,17 @@ private void AddLastContractBuildableAttributes( continue; } - var attributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute)); - var newAttributeStatement = new AttributeStatement(attributeType, TypeOf(resolvedProvider.Type)); + var newAttributeStatement = new AttributeStatement(s_buildableAttributeType, TypeOf(resolvedProvider.Type)); if (isOutputLibraryType) { // For output-library types, reconstruct through the suppression-handling path so that // [Experimental] and [Obsolete] diagnostics are properly suppressed, consistent with // how generated attributes are emitted. - string experimentalTypeJustification = $"{resolvedProvider.Type} is experimental and may change in future versions."; - string obsoleteTypeJustification = $"{resolvedProvider.Type} is obsolete and may be removed in future versions."; AddAttributeForType( attributes, newAttributeStatement, - resolvedProvider, - experimentalTypeJustification, - obsoleteTypeJustification); + resolvedProvider); } else { @@ -230,7 +208,7 @@ private static bool IsBuildableAttribute(MethodBodyStatement statement) _ => null }; - return attribute?.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)) == true; + return attribute?.Type.Equals(s_buildableAttributeType) == true; } private HashSet GetCustomizedBuildableTypes() @@ -240,7 +218,7 @@ private HashSet GetCustomizedBuildableTypes() { if (!string.Equals( attribute.Type.FullyQualifiedName, - typeof(ModelReaderWriterBuildableAttribute).FullName, + s_buildableAttributeType.FullyQualifiedName, StringComparison.Ordinal)) { continue; @@ -266,21 +244,6 @@ private static string GetTypeIdentity(CSharpType type) : $"{name}<{string.Join(",", type.Arguments.Select(GetTypeIdentity))}>"; } - // Returns the CLR metadata simple name with arity suffix for generic types (e.g. "Type`1"), - // mirroring NamedTypeSymbolProvider.GetMetadataName which uses symbol.MetadataName. - private static string GetClrSimpleMetadataName(CSharpType type) - => type.Arguments.Count > 0 ? $"{type.Name}`{type.Arguments.Count}" : type.Name; - - // Returns the full CLR declaring-type chain using '+' separators (e.g. "Outer`1+Middle"), - // mirroring the recursive NamedTypeSymbolProvider.GetMetadataName pattern. - private static string GetClrDeclaringTypeChain(CSharpType type) - { - var simpleName = GetClrSimpleMetadataName(type); - return type.DeclaringType is null - ? simpleName - : $"{GetClrDeclaringTypeChain(type.DeclaringType)}+{simpleName}"; - } - /// /// Collects all types that implement IPersistableModel, including all models and their properties /// that are also IPersistableModel types, recursively without duplicates. @@ -661,9 +624,7 @@ private static bool HasWritableModelReaderWriterSerialization(TypeProvider provi private static void AddAttributeForType( Dictionary attributes, AttributeStatement attributeStatement, - TypeProvider typeProvider, - string experimentalTypeJustification, - string obsoleteTypeJustification) + TypeProvider typeProvider) { AttributeStatement? experimentalOrObsoleteAttribute = typeProvider.CanonicalView.Attributes .FirstOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute)) || a.Type.Equals(typeof(ObsoleteAttribute))); @@ -672,11 +633,13 @@ private static void AddAttributeForType( if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ExperimentalAttribute)) == true) { - attributes.Add(key, new SuppressionStatement(attributeStatement, experimentalOrObsoleteAttribute.Arguments[0], experimentalTypeJustification)); + string justification = $"{typeProvider.Type} is experimental and may change in future versions."; + attributes.Add(key, new SuppressionStatement(attributeStatement, experimentalOrObsoleteAttribute.Arguments[0], justification)); } else if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ObsoleteAttribute)) == true) { - attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(DefaultObsoleteDiagnosticId), obsoleteTypeJustification)); + string justification = $"{typeProvider.Type} is obsolete and may be removed in future versions."; + attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(DefaultObsoleteDiagnosticId), justification)); } else { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs index f2bc01a85e0..c5d02cfb0d1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs @@ -181,6 +181,20 @@ internal CSharpType( ? $"{Namespace}.{Name}" : $"{Namespace}.{DeclaringType.Name}.{Name}"; public CSharpType? DeclaringType { get; private init; } + + /// + /// Returns the CLR metadata name for this type, including the arity suffix for generic types + /// (e.g., Type`1) and the +-separated declaring-type chain for nested types + /// (e.g., Outer`1+Inner). This format is compatible with + /// . + /// + public string GetClrMetadataName() + { + var simpleName = Arguments.Count > 0 ? $"{Name}`{Arguments.Count}" : Name; + return DeclaringType is null + ? simpleName + : $"{DeclaringType.GetClrMetadataName()}+{simpleName}"; + } public bool IsValueType { get; private init; } public bool IsEnum => _underlyingType is not null; public bool IsLiteral => _literal is not null; From ffd81e2583913af985b3efc97c98dda6282e0b3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:01:15 +0000 Subject: [PATCH 08/11] Fix AddLastContractBuildableAttributes: preserve constructed targetType for key and emission Use targetType (the constructed type from the last contract) for both the dictionary key and the emitted TypeOf(...) expression, instead of resolvedProvider.Type (the generic type definition). This ensures: - typeof(Foo) is emitted instead of typeof(Foo) - Foo and Foo are treated as distinct entries (not deduplicated) Also refactor AddAttributeForType(TypeProvider) to accept an explicit key so the suppression-handling path uses the targetType-derived key when called from AddLastContractBuildableAttributes. Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 341a31268e5..e0bc9fdec86 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -154,16 +154,16 @@ private void AddLastContractBuildableAttributes( isOutputLibraryType = false; } - // Deduplicate using the resolved provider's normalized type identity so that both the - // attributes dictionary check and the customized-buildable check use the same key source. - var typeKey = resolvedProvider.Type.FullyQualifiedName; - var resolvedIdentity = GetTypeIdentity(resolvedProvider.Type); - if (attributes.ContainsKey(typeKey) || customizedBuildableTypes.Contains(resolvedIdentity)) + // Use targetType (the original constructed type from the last contract) for both the key and the + // emitted typeof(...) expression, so that generic instantiations such as Foo and Foo + // are treated as distinct entries and the emitted attribute preserves the type arguments. + var typeKey = GetTypeIdentity(targetType); + if (attributes.ContainsKey(typeKey) || customizedBuildableTypes.Contains(typeKey)) { continue; } - var newAttributeStatement = new AttributeStatement(s_buildableAttributeType, TypeOf(resolvedProvider.Type)); + var newAttributeStatement = new AttributeStatement(s_buildableAttributeType, TypeOf(targetType)); if (isOutputLibraryType) { @@ -173,7 +173,8 @@ private void AddLastContractBuildableAttributes( AddAttributeForType( attributes, newAttributeStatement, - resolvedProvider); + resolvedProvider, + typeKey); } else { @@ -625,12 +626,17 @@ private static void AddAttributeForType( Dictionary attributes, AttributeStatement attributeStatement, TypeProvider typeProvider) + => AddAttributeForType(attributes, attributeStatement, typeProvider, typeProvider.Type.FullyQualifiedName); + + private static void AddAttributeForType( + Dictionary attributes, + AttributeStatement attributeStatement, + TypeProvider typeProvider, + string key) { AttributeStatement? experimentalOrObsoleteAttribute = typeProvider.CanonicalView.Attributes .FirstOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute)) || a.Type.Equals(typeof(ObsoleteAttribute))); - var key = typeProvider.Type.FullyQualifiedName; - if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ExperimentalAttribute)) == true) { string justification = $"{typeProvider.Type} is experimental and may change in future versions."; From c7620827ec2add2c7878a344e1584b20cf6c2fdc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:26:03 +0000 Subject: [PATCH 09/11] Fix suppression for restored last-contract buildable attributes; add ClrMetadataName property tests and ObsoleteCustomType suppression test Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 44 ++++++++-------- ...ModelReaderWriterContextDefinitionTests.cs | 31 +++++++++++ .../ObsoleteCustomModel.cs | 9 ++++ ...tesForObsoleteCustomTypeHaveSuppression.cs | 17 +++++++ .../SampleContext.cs | 23 +++++++++ .../src/Primitives/CSharpType.cs | 9 ++-- .../test/Primitives/CSharpTypeTests.cs | 51 +++++++++++++++++++ 7 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression(Custom)/ObsoleteCustomModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression/SampleContext.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index e0bc9fdec86..1e8f246cd05 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -128,7 +128,6 @@ private void AddLastContractBuildableAttributes( var identity = GetTypeIdentity(targetType); - bool isOutputLibraryType; TypeProvider? resolvedProvider; if (outputLibraryProviders.TryGetValue(identity, out var outputLibraryProvider)) { @@ -137,13 +136,12 @@ private void AddLastContractBuildableAttributes( continue; } resolvedProvider = outputLibraryProvider; - isOutputLibraryType = true; } else { resolvedProvider = ScmCodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( targetType.Namespace, - targetType.GetClrMetadataName(), + targetType.ClrMetadataName, null, includeReferencedAssemblies: true); @@ -151,7 +149,6 @@ private void AddLastContractBuildableAttributes( { continue; } - isOutputLibraryType = false; } // Use targetType (the original constructed type from the last contract) for both the key and the @@ -165,25 +162,15 @@ private void AddLastContractBuildableAttributes( var newAttributeStatement = new AttributeStatement(s_buildableAttributeType, TypeOf(targetType)); - if (isOutputLibraryType) - { - // For output-library types, reconstruct through the suppression-handling path so that - // [Experimental] and [Obsolete] diagnostics are properly suppressed, consistent with - // how generated attributes are emitted. - AddAttributeForType( - attributes, - newAttributeStatement, - resolvedProvider, - typeKey); - } - else - { - // For types resolved from the customization layer or referenced assemblies, - // add the attribute directly; their symbol model may not be fully representable - // through the generator's expression tree (e.g. BCL types), so skip the - // CanonicalView-based suppression path that is designed for output-library types. - attributes.Add(typeKey, newAttributeStatement); - } + // Route through the suppression-handling path so that [Experimental] and [Obsolete] + // diagnostics are properly suppressed. NamedTypeSymbolProvider (returned for customization + // and referenced-assembly types) exposes symbol attributes through CanonicalView.Attributes, + // so the same suppression check works for all resolved providers. + AddAttributeForType( + attributes, + newAttributeStatement, + resolvedProvider, + typeKey); } } @@ -634,7 +621,16 @@ private static void AddAttributeForType( TypeProvider typeProvider, string key) { - AttributeStatement? experimentalOrObsoleteAttribute = typeProvider.CanonicalView.Attributes + // Use CanonicalView only when the provider has a custom-code layer, since CanonicalTypeProvider + // merges generated and customization attributes. For symbol-backed providers (NamedTypeSymbolProvider, + // which have no custom-code layer), use Attributes directly to avoid DeduplicateAttributes invoking + // ToDisplayString() on BCL/framework attributes that contain literal argument types not handled by + // LiteralExpression.Write (e.g. uint, byte). This is safe because symbol providers never merge + // a separate custom-code view. + var sourceAttributes = typeProvider.CustomCodeView != null + ? typeProvider.CanonicalView.Attributes + : typeProvider.Attributes; + AttributeStatement? experimentalOrObsoleteAttribute = sourceAttributes .FirstOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute)) || a.Type.Equals(typeof(ObsoleteAttribute))); if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ExperimentalAttribute)) == true) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 11bc50962cc..6881e50d721 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -2075,6 +2075,37 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public async Task LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression() + { + // The last contract declared a buildable attribute for ObsoleteCustomModel. The type is defined + // only in the customization layer (not produced by the current generation) with [Obsolete]. + // The restored attribute must be wrapped in #pragma warning disable CS0618 suppressions. + var regularModel = InputFactory.Model("RegularModel", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [regularModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")), + "RegularModel is produced by the current generation and must appear exactly once"); + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("ObsoleteCustomModel")), + "ObsoleteCustomModel is in the customization layer and must be restored for back-compat"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + // Buildable attributes restored from the last contract are symbol-based (IsFrameworkType == false), so // match by fully qualified name to cover both generated and restored entries. private static List GetBuildableAttributes(ModelReaderWriterContextDefinition contextDefinition) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression(Custom)/ObsoleteCustomModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression(Custom)/ObsoleteCustomModel.cs new file mode 100644 index 00000000000..bbdf7f72bb7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression(Custom)/ObsoleteCustomModel.cs @@ -0,0 +1,9 @@ +using System; + +namespace Sample.Models +{ + [Obsolete("This type is obsolete.")] + public class ObsoleteCustomModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression.cs new file mode 100644 index 00000000000..acfdcd30aca --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression.cs @@ -0,0 +1,17 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ +#pragma warning disable CS0618 // global::Sample.Models.ObsoleteCustomModel is obsolete and may be removed in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.ObsoleteCustomModel))] +#pragma warning restore CS0618 // global::Sample.Models.ObsoleteCustomModel is obsolete and may be removed in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression/SampleContext.cs new file mode 100644 index 00000000000..2248e2419a7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForObsoleteCustomTypeHaveSuppression/SampleContext.cs @@ -0,0 +1,23 @@ +using System; +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.ObsoleteCustomModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class RegularModel + { + } + + [Obsolete("This type is obsolete.")] + public class ObsoleteCustomModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs index c5d02cfb0d1..2ec3c86dc41 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs @@ -42,6 +42,7 @@ public class CSharpType private bool? _isIAsyncEnumerableOfT; private bool? _containsBinaryData; private int? _hashCode; + private string? _clrMetadataName; private CSharpType? _propertyInitializationType; private CSharpType? _elementType; private CSharpType? _inputType; @@ -183,17 +184,19 @@ internal CSharpType( public CSharpType? DeclaringType { get; private init; } /// - /// Returns the CLR metadata name for this type, including the arity suffix for generic types + /// Gets the CLR metadata name for this type, including the arity suffix for generic types /// (e.g., Type`1) and the +-separated declaring-type chain for nested types /// (e.g., Outer`1+Inner). This format is compatible with /// . /// - public string GetClrMetadataName() + public string ClrMetadataName => _clrMetadataName ??= BuildClrMetadataName(); + + private string BuildClrMetadataName() { var simpleName = Arguments.Count > 0 ? $"{Name}`{Arguments.Count}" : Name; return DeclaringType is null ? simpleName - : $"{DeclaringType.GetClrMetadataName()}+{simpleName}"; + : $"{DeclaringType.ClrMetadataName}+{simpleName}"; } public bool IsValueType { get; private init; } public bool IsEnum => _underlyingType is not null; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Primitives/CSharpTypeTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Primitives/CSharpTypeTests.cs index eb18e3e8be0..f1ad231f280 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Primitives/CSharpTypeTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Primitives/CSharpTypeTests.cs @@ -661,5 +661,56 @@ private class TestDerivedType : TestBaseType private class TestBaseType { } + + [TestCase("MyNs", "SimpleType", ExpectedResult = "SimpleType")] + [TestCase("MyNs", "SimpleType`1", ExpectedResult = "SimpleType`1")] + public string ClrMetadataName_SimpleType(string ns, string name) + { + var type = new CSharpType(name, ns, false, false, null, [], true, false); + return type.ClrMetadataName; + } + + [Test] + public void ClrMetadataName_GenericType_OneTypeArg() + { + var argType = new CSharpType("T", "MyNs", false, false, null, [], true, false); + var type = new CSharpType("GenericType", "MyNs", false, false, null, [argType], true, false); + Assert.AreEqual("GenericType`1", type.ClrMetadataName); + } + + [Test] + public void ClrMetadataName_GenericType_TwoTypeArgs() + { + var arg1 = new CSharpType("TKey", "MyNs", false, false, null, [], true, false); + var arg2 = new CSharpType("TValue", "MyNs", false, false, null, [], true, false); + var type = new CSharpType("DictionaryType", "MyNs", false, false, null, [arg1, arg2], true, false); + Assert.AreEqual("DictionaryType`2", type.ClrMetadataName); + } + + [Test] + public void ClrMetadataName_NestedType() + { + var outerType = new CSharpType("Outer", "MyNs", false, false, null, [], true, false); + var innerType = new CSharpType("Inner", "MyNs", false, false, outerType, [], true, false); + Assert.AreEqual("Outer+Inner", innerType.ClrMetadataName); + } + + [Test] + public void ClrMetadataName_GenericNestedType() + { + var arg = new CSharpType("T", "MyNs", false, false, null, [], true, false); + var outerType = new CSharpType("Outer", "MyNs", false, false, null, [arg], true, false); + var innerType = new CSharpType("Inner", "MyNs", false, false, outerType, [], true, false); + Assert.AreEqual("Outer`1+Inner", innerType.ClrMetadataName); + } + + [Test] + public void ClrMetadataName_LazilyComputedAndCached() + { + var type = new CSharpType("MyType", "MyNs", false, false, null, [], true, false); + var first = type.ClrMetadataName; + var second = type.ClrMetadataName; + Assert.AreSame(first, second); + } } } From b93a24e9223b3761d52a3d525f48a297d7179c4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:38:52 +0000 Subject: [PATCH 10/11] Address PR feedback: remove comments, fix typeKey duplication, always use CanonicalView, add experimental/both-attributes tests - Remove comment before AddLastContractBuildableAttributes call - Remove comment before new attribute statement - Remove redundant typeKey computation (identity already computed at line 129) - Fix AddAttributeForType to always use typeProvider.CanonicalView.Attributes - Fix LiteralExpression.Write() to handle byte, sbyte, short, ushort, uint, ulong (needed for CanonicalView.Attributes to work on BCL types with compiler-emitted attrs) - Add LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression test - Add LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression test (verifies experimental takes precedence when both attrs are present) Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 25 +------- ...ModelReaderWriterContextDefinitionTests.cs | 63 +++++++++++++++++++ .../ExperimentalObsoleteModel.cs | 11 ++++ ...talAndObsoleteCustomTypeHaveSuppression.cs | 17 +++++ .../SampleContext.cs | 21 +++++++ .../ExperimentalCustomModel.cs | 9 +++ ...orExperimentalCustomTypeHaveSuppression.cs | 17 +++++ .../SampleContext.cs | 21 +++++++ .../src/Expressions/LiteralExpression.cs | 6 ++ 9 files changed, 168 insertions(+), 22 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression(Custom)/ExperimentalObsoleteModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression(Custom)/ExperimentalCustomModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression/SampleContext.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 1e8f246cd05..45426518c8a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -87,8 +87,6 @@ protected override IReadOnlyList BuildAttributes() provider); } - // Back-compat: restore any ModelReaderWriterBuildableAttribute that was present in the last contract - // but is missing from the freshly generated set, without introducing duplicates. AddLastContractBuildableAttributes(attributes, customizedBuildableTypes); // Sort by the simple type name (last part after the last dot) instead of the fully qualified name @@ -151,26 +149,18 @@ private void AddLastContractBuildableAttributes( } } - // Use targetType (the original constructed type from the last contract) for both the key and the - // emitted typeof(...) expression, so that generic instantiations such as Foo and Foo - // are treated as distinct entries and the emitted attribute preserves the type arguments. - var typeKey = GetTypeIdentity(targetType); - if (attributes.ContainsKey(typeKey) || customizedBuildableTypes.Contains(typeKey)) + if (attributes.ContainsKey(identity) || customizedBuildableTypes.Contains(identity)) { continue; } var newAttributeStatement = new AttributeStatement(s_buildableAttributeType, TypeOf(targetType)); - // Route through the suppression-handling path so that [Experimental] and [Obsolete] - // diagnostics are properly suppressed. NamedTypeSymbolProvider (returned for customization - // and referenced-assembly types) exposes symbol attributes through CanonicalView.Attributes, - // so the same suppression check works for all resolved providers. AddAttributeForType( attributes, newAttributeStatement, resolvedProvider, - typeKey); + identity); } } @@ -621,16 +611,7 @@ private static void AddAttributeForType( TypeProvider typeProvider, string key) { - // Use CanonicalView only when the provider has a custom-code layer, since CanonicalTypeProvider - // merges generated and customization attributes. For symbol-backed providers (NamedTypeSymbolProvider, - // which have no custom-code layer), use Attributes directly to avoid DeduplicateAttributes invoking - // ToDisplayString() on BCL/framework attributes that contain literal argument types not handled by - // LiteralExpression.Write (e.g. uint, byte). This is safe because symbol providers never merge - // a separate custom-code view. - var sourceAttributes = typeProvider.CustomCodeView != null - ? typeProvider.CanonicalView.Attributes - : typeProvider.Attributes; - AttributeStatement? experimentalOrObsoleteAttribute = sourceAttributes + AttributeStatement? experimentalOrObsoleteAttribute = typeProvider.CanonicalView.Attributes .FirstOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute)) || a.Type.Equals(typeof(ObsoleteAttribute))); if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ExperimentalAttribute)) == true) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 6881e50d721..4685d2e0489 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -2106,6 +2106,69 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public async Task LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression() + { + // The last contract declared a buildable attribute for ExperimentalCustomModel. The type is defined + // only in the customization layer (not produced by the current generation) with [Experimental]. + // The restored attribute must be wrapped in #pragma warning disable suppressions. + var regularModel = InputFactory.Model("RegularModel", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [regularModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")), + "RegularModel is produced by the current generation and must appear exactly once"); + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("ExperimentalCustomModel")), + "ExperimentalCustomModel is in the customization layer and must be restored for back-compat"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression() + { + // The last contract declared a buildable attribute for ExperimentalObsoleteModel. The type is defined + // only in the customization layer with both [Experimental] and [Obsolete]. + // When both attributes are present, [Experimental] (declared first) takes precedence and the + // restored attribute must be wrapped in experimental suppressions. + var regularModel = InputFactory.Model("RegularModel", properties: + [ + InputFactory.Property("Property1", InputPrimitiveType.String) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [regularModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = GetBuildableAttributes(contextDefinition); + + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("RegularModel")), + "RegularModel is produced by the current generation and must appear exactly once"); + Assert.AreEqual(1, buildableAttributes + .Count(a => a.Arguments.First().ToDisplayString().Contains("ExperimentalObsoleteModel")), + "ExperimentalObsoleteModel is in the customization layer and must be restored for back-compat"); + + var writer = new TypeProviderWriter(contextDefinition); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + // Buildable attributes restored from the last contract are symbol-based (IsFrameworkType == false), so // match by fully qualified name to cover both generated and restored entries. private static List GetBuildableAttributes(ModelReaderWriterContextDefinition contextDefinition) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression(Custom)/ExperimentalObsoleteModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression(Custom)/ExperimentalObsoleteModel.cs new file mode 100644 index 00000000000..544c6776fec --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression(Custom)/ExperimentalObsoleteModel.cs @@ -0,0 +1,11 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Sample.Models +{ + [Experimental("TEST002")] + [Obsolete("This type is obsolete.")] + public class ExperimentalObsoleteModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression.cs new file mode 100644 index 00000000000..3bad5fe990f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression.cs @@ -0,0 +1,17 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ +#pragma warning disable TEST002 // global::Sample.Models.ExperimentalObsoleteModel is experimental and may change in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.ExperimentalObsoleteModel))] +#pragma warning restore TEST002 // global::Sample.Models.ExperimentalObsoleteModel is experimental and may change in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression/SampleContext.cs new file mode 100644 index 00000000000..fa84b3ba47c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalAndObsoleteCustomTypeHaveSuppression/SampleContext.cs @@ -0,0 +1,21 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.ExperimentalObsoleteModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class RegularModel + { + } + + public class ExperimentalObsoleteModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression(Custom)/ExperimentalCustomModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression(Custom)/ExperimentalCustomModel.cs new file mode 100644 index 00000000000..545b4f63df8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression(Custom)/ExperimentalCustomModel.cs @@ -0,0 +1,9 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sample.Models +{ + [Experimental("TEST001")] + public class ExperimentalCustomModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression.cs new file mode 100644 index 00000000000..2e9a1097389 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression.cs @@ -0,0 +1,17 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ +#pragma warning disable TEST001 // global::Sample.Models.ExperimentalCustomModel is experimental and may change in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.ExperimentalCustomModel))] +#pragma warning restore TEST001 // global::Sample.Models.ExperimentalCustomModel is experimental and may change in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression/SampleContext.cs new file mode 100644 index 00000000000..58a42a4d7d2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/LastContractBuildableAttributesForExperimentalCustomTypeHaveSuppression/SampleContext.cs @@ -0,0 +1,21 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Sample.Models.RegularModel))] + [ModelReaderWriterBuildable(typeof(Sample.Models.ExperimentalCustomModel))] + public partial class SampleContext + { + } +} + +namespace Sample.Models +{ + public partial class RegularModel + { + } + + public class ExperimentalCustomModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/LiteralExpression.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/LiteralExpression.cs index bb67f001301..0b6f139f1c7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/LiteralExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/LiteralExpression.cs @@ -19,7 +19,13 @@ internal override void Write(CodeWriter writer) null => "null", string s => SyntaxFactory.Literal(s).ToString(), int i => SyntaxFactory.Literal(i).ToString(), + uint ui => SyntaxFactory.Literal(ui).ToString(), long l => SyntaxFactory.Literal(l).ToString(), + ulong ul => SyntaxFactory.Literal(ul).ToString(), + byte b => SyntaxFactory.Literal((int)b).ToString(), + sbyte sb => SyntaxFactory.Literal((int)sb).ToString(), + short s => SyntaxFactory.Literal((int)s).ToString(), + ushort us => SyntaxFactory.Literal((uint)us).ToString(), decimal d => SyntaxFactory.Literal(d).ToString(), double d => SyntaxFactory.Literal(d).ToString(), float f => SyntaxFactory.Literal(f).ToString(), From 630e1e81918bf1f23e0b76b4ba83e8a6bffbc44b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:00:45 +0000 Subject: [PATCH 11/11] Remove single-arg AddAttributeForType overload; add LiteralExpression tests for uint/ulong/byte/sbyte/short/ushort Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelReaderWriterContextDefinition.cs | 9 +-- .../Expressions/LiteralExpressionTests.cs | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Expressions/LiteralExpressionTests.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 45426518c8a..14e4ddef553 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -84,7 +84,8 @@ protected override IReadOnlyList BuildAttributes() AddAttributeForType( attributes, attributeStatement, - provider); + provider, + provider.Type.FullyQualifiedName); } AddLastContractBuildableAttributes(attributes, customizedBuildableTypes); @@ -599,12 +600,6 @@ private static bool HasWritableModelReaderWriterSerialization(TypeProvider provi .Any(ShouldWriteProvider); } - private static void AddAttributeForType( - Dictionary attributes, - AttributeStatement attributeStatement, - TypeProvider typeProvider) - => AddAttributeForType(attributes, attributeStatement, typeProvider, typeProvider.Type.FullyQualifiedName); - private static void AddAttributeForType( Dictionary attributes, AttributeStatement attributeStatement, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Expressions/LiteralExpressionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Expressions/LiteralExpressionTests.cs new file mode 100644 index 00000000000..5d4d43db3fa --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Expressions/LiteralExpressionTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.TypeSpec.Generator.Expressions; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.Tests.Expressions +{ + internal class LiteralExpressionTests + { + [TestCase((uint)42, "42U")] + [TestCase(uint.MaxValue, "4294967295U")] + public void Write_UInt(uint value, string expected) + { + var expression = new LiteralExpression(value); + using var writer = new CodeWriter(); + expression.Write(writer); + + Assert.AreEqual(expected, writer.ToString(false)); + } + + [TestCase((ulong)42, "42UL")] + [TestCase(ulong.MaxValue, "18446744073709551615UL")] + public void Write_ULong(ulong value, string expected) + { + var expression = new LiteralExpression(value); + using var writer = new CodeWriter(); + expression.Write(writer); + + Assert.AreEqual(expected, writer.ToString(false)); + } + + [TestCase((byte)0, "0")] + [TestCase((byte)42, "42")] + [TestCase(byte.MaxValue, "255")] + public void Write_Byte(byte value, string expected) + { + var expression = new LiteralExpression(value); + using var writer = new CodeWriter(); + expression.Write(writer); + + Assert.AreEqual(expected, writer.ToString(false)); + } + + [TestCase((sbyte)42, "42")] + [TestCase((sbyte)-42, "-42")] + [TestCase(sbyte.MinValue, "-128")] + public void Write_SByte(sbyte value, string expected) + { + var expression = new LiteralExpression(value); + using var writer = new CodeWriter(); + expression.Write(writer); + + Assert.AreEqual(expected, writer.ToString(false)); + } + + [TestCase((short)42, "42")] + [TestCase((short)-42, "-42")] + [TestCase(short.MinValue, "-32768")] + public void Write_Short(short value, string expected) + { + var expression = new LiteralExpression(value); + using var writer = new CodeWriter(); + expression.Write(writer); + + Assert.AreEqual(expected, writer.ToString(false)); + } + + [TestCase((ushort)42, "42U")] + [TestCase(ushort.MaxValue, "65535U")] + public void Write_UShort(ushort value, string expected) + { + var expression = new LiteralExpression(value); + using var writer = new CodeWriter(); + expression.Write(writer); + + Assert.AreEqual(expected, writer.ToString(false)); + } + } +}