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 0b6f139f1c7..61e86532e1c 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 @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; namespace Microsoft.TypeSpec.Generator.Expressions @@ -14,26 +15,43 @@ public sealed record LiteralExpression(object? Literal) : ValueExpression { internal override void Write(CodeWriter writer) { - writer.AppendRaw(Literal switch + writer.AppendRaw(Format(Literal) ?? throw new NotImplementedException()); + } + + /// + /// Creates a when maps to a renderable literal. + /// + internal static bool TryCreate(object? value, [NotNullWhen(true)] out LiteralExpression? literal) + { + if (Format(value) is null) { - 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(), - char c => SyntaxFactory.Literal(c).ToString(), - bool b => b ? "true" : "false", - BinaryData bd => bd.ToArray().Length == 0 ? "new byte[] { }" : SyntaxFactory.Literal(bd.ToString()).ToString(), - _ => throw new NotImplementedException() - }); + literal = null; + return false; + } + + literal = new LiteralExpression(value); + return true; } + + private static string? Format(object? literal) => literal switch + { + 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(), + char c => SyntaxFactory.Literal(c).ToString(), + bool b => b ? "true" : "false", + BinaryData bd => bd.ToArray().Length == 0 ? "new byte[] { }" : SyntaxFactory.Literal(bd.ToString()).ToString(), + _ => null + }; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/LibraryVisitor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/LibraryVisitor.cs index 03df192714c..9528d3f308f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/LibraryVisitor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/LibraryVisitor.cs @@ -297,7 +297,7 @@ protected internal virtual FinallyExpression VisitFinallyExpression(FinallyExpre /// /// The original . /// Null if it should be removed otherwise the modified version of the . - protected virtual PropertyProvider? VisitProperty(PropertyProvider property) + protected internal virtual PropertyProvider? VisitProperty(PropertyProvider property) { return property; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 81ee9fcd288..9d0c903ef53 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -233,7 +233,8 @@ .. _assemblyMetadataReferences.Value.Concat(CodeModelGenerator.Instance.Addition project = project .AddMetadataReferences(metadataReferences) .WithCompilationOptions(new CSharpCompilationOptions( - OutputKind.DynamicallyLinkedLibrary, metadataReferenceResolver: _metadataReferenceResolver.Value, nullableContextOptions: NullableContextOptions.Disable)); + OutputKind.DynamicallyLinkedLibrary, metadataReferenceResolver: _metadataReferenceResolver.Value, nullableContextOptions: NullableContextOptions.Disable) + .WithMetadataImportOptions(MetadataImportOptions.All)); return await project.GetCompilationAsync(); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs index e8496d8987e..62453e8510b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs @@ -68,7 +68,7 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputType?.N protected static string RemoveUnderscores(string name) => name.Replace("_", string.Empty); private HashSet? _customMemberNames; - private HashSet CustomMemberNames => _customMemberNames ??= new HashSet( + private protected HashSet CustomMemberNames => _customMemberNames ??= new HashSet( GetCustomMemberNames(), StringComparer.OrdinalIgnoreCase); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs index c849dcf2a1b..4276c01550f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; @@ -274,5 +276,81 @@ protected override TypeProvider[] BuildSerializationProviders() return CodeModelGenerator.Instance.TypeFactory.CreateSerializations(_inputType, this).ToArray(); } protected override bool GetIsEnum() => true; + + protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) + { + var lastContractProperties = LastContractView?.Properties; + if (lastContractProperties == null || lastContractProperties.Count == 0) + { + return null; + } + + var currentNames = new HashSet(currentValues.Select(v => v.Name), StringComparer.OrdinalIgnoreCase); + var lastContractValueFields = new Dictionary(StringComparer.Ordinal); + foreach (var field in LastContractView!.Fields) + { + lastContractValueFields.TryAdd(field.Name, field); + } + + List? restoredMembers = null; + foreach (var property in lastContractProperties) + { + // Members that still exist in the current spec or are provided by custom code are left untouched. + if (currentNames.Contains(property.Name) || CustomMemberNames.Contains(property.Name)) + { + continue; + } + + // Honor an intentional removal recorded in the ApiCompat baseline. + if (CodeModelGenerator.Instance.SourceInputModel?.ApiCompatBaseline.IsMemberSuppressed(Type.FullyQualifiedName, property.Name, 0) == true) + { + CodeModelGenerator.Instance.Emitter.Debug( + $"Skipping re-add of enum member '{Name}.{property.Name}'; the removal is accepted in the ApiCompat baseline.", + BackCompatibilityChangeCategory.BaselineAcceptedRemovalSkipped); + continue; + } + + if (TryResurrectRemovedMember(property, lastContractValueFields, out var resurrectedMember)) + { + (restoredMembers ??= []).Add(resurrectedMember); + CodeModelGenerator.Instance.Emitter.Debug( + $"Re-added enum member '{property.Name}' to enum '{Name}' to preserve a member from the last contract.", + BackCompatibilityChangeCategory.EnumMemberAddedFromLastContract); + } + } + + if (restoredMembers == null) + { + return null; + } + + // Preserve the current spec order and append the restored members at the end. + return [.. currentValues, .. restoredMembers]; + } + + private bool TryResurrectRemovedMember( + PropertyProvider lastContractProperty, + IReadOnlyDictionary lastContractValueFields, + [NotNullWhen(true)] out EnumTypeMember? member) + { + member = null; + + // The wire value lives in the private const `Value` field. + var valueFieldName = $"{lastContractProperty.Name}Value"; + if (!lastContractValueFields.TryGetValue(valueFieldName, out var valueField) + || valueField.InitializationValue is not LiteralExpression { Literal: { } literalValue }) + { + return false; + } + + var field = new FieldProvider( + FieldModifiers.Private | FieldModifiers.Const, + EnumUnderlyingType, + valueFieldName, + this, + initializationValue: Literal(literalValue)); + member = new EnumTypeMember(lastContractProperty.Name, field, literalValue); + return true; + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index b3f7bb8dbd8..cbf8eca76be 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -247,18 +247,13 @@ protected internal override PropertyProvider[] BuildProperties() return null; } - private static ValueExpression? GetFieldInitializer(IFieldSymbol fieldSymbol) + private static LiteralExpression? GetFieldInitializer(IFieldSymbol fieldSymbol) { - if (fieldSymbol.ContainingType?.TypeKind == TypeKind.Enum) - { - if (fieldSymbol.HasConstantValue && fieldSymbol.ConstantValue != null) - { - return Literal(fieldSymbol.ConstantValue); - } - return null; - } - - return null; + return fieldSymbol.HasConstantValue && + fieldSymbol.ConstantValue != null && + LiteralExpression.TryCreate(fieldSymbol.ConstantValue, out var initializer) + ? initializer + : null; } private static string? GetOriginalName(ISymbol symbol) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 5bf5cd615b6..5ecd0b10836 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -825,7 +825,8 @@ internal void ProcessTypeForBackCompatibility() IReadOnlyList? updatedEnumValues = null; IEnumerable? newFields = null; - if (this is EnumProvider) + IEnumerable? newProperties = null; + if (this is EnumProvider enumProvider) { var hasFields = LastContractView?.Fields != null && LastContractView.Fields.Count > 0; if (hasFields) @@ -848,7 +849,39 @@ internal void ProcessTypeForBackCompatibility() updatedEnumValues = newEnumValues; } - newFields = filteredFields; + // Sync the enum values before rebuilding the member collections from them. + if (updatedEnumValues != null) + { + _enumValues = updatedEnumValues; + } + + if (enumProvider.IsExtensible) + { + // Extensible enums carry an extra backing `_value` field and surface members + // as properties, so rebuild both from the updated members. Reuse the + // already-visited field and property instances for members that still exist so + // any visitor mutations are preserved and only the restored members are + // (re)visited below. + var existingFields = new Dictionary(StringComparer.Ordinal); + foreach (var field in Fields) + { + existingFields.TryAdd(field.Name, field); + } + newFields = ApplyCustomizationFilter( + BuildFields().Select(f => existingFields.TryGetValue(f.Name, out var existing) ? existing : f)); + + var existingProperties = new Dictionary(StringComparer.Ordinal); + foreach (var property in Properties) + { + existingProperties.TryAdd(property.Name, property); + } + newProperties = ApplyCustomizationFilter( + BuildProperties().Select(p => existingProperties.TryGetValue(p.Name, out var existing) ? existing : p)); + } + else + { + newFields = filteredFields; + } } } } @@ -856,13 +889,8 @@ internal void ProcessTypeForBackCompatibility() var newMethods = hasMethods ? BuildMethodsForBackCompatibility(Methods) : null; var newConstructors = hasConstructors ? BuildConstructorsForBackCompatibility(Constructors) : null; - if (newFields != null || newMethods != null || newConstructors != null) + if (newFields != null || newProperties != null || newMethods != null || newConstructors != null) { - if (updatedEnumValues != null) - { - _enumValues = updatedEnumValues; - } - // Back-compatibility processing intentionally runs after the library visitor pass so // that the contract comparison uses the final, post-visitor member signatures (otherwise // we could incorrectly decide whether a back-compat member is needed). As a result, any @@ -877,12 +905,16 @@ internal void ProcessTypeForBackCompatibility() { newConstructors = VisitNewMembers(newConstructors, Constructors, static (member, visitor) => visitor.VisitConstructor(member)); } + if (newProperties != null) + { + newProperties = VisitNewMembers(newProperties, Properties, static (member, visitor) => visitor.VisitProperty(member)); + } if (newFields != null) { newFields = VisitNewMembers(newFields, Fields, static (member, visitor) => visitor.VisitField(member)); } - Update(fields: newFields, methods: newMethods, constructors: newConstructors); + Update(fields: newFields, properties: newProperties, methods: newMethods, constructors: newConstructors); } // Providers whose attributes depend on final generation decisions build their attributes at write diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs index 49b40624443..dd59b12dd58 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs @@ -494,7 +494,7 @@ private class TestFilterVisitor : LibraryVisitor return constructor; } - protected override PropertyProvider? VisitProperty(PropertyProvider property) + protected internal override PropertyProvider? VisitProperty(PropertyProvider property) { if (property.Name == "TestProperty") { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs index eb341de30b5..34afc34392e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs @@ -650,6 +650,168 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsNull(fields[1].InitializationValue); } + // Validates that a member removed from an extensible (string-backed) enum is re-added from the + // last contract. Unlike fixed string enums, an extensible enum stores its wire value in a private + // const `Value` field, so the value is recoverable (even from a compiled assembly's + // metadata) and the previously shipped member can be restored to avoid a source-breaking removal. + [Test] + public async Task BackCompat_ExtensibleEnumRemovedValueReadded() + { + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(string), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // Last contract: Default, Recover, Third. Current input removes "Third". + var input = InputFactory.StringEnum("mockInputEnum", [ + ("Default", "default"), + ("Recover", "recover"), + ], isExtensible: true); + + var enumType = EnumProvider.Create(input); + Assert.IsFalse(enumType is ApiVersionEnumProvider); + + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + // "Third" is re-added (appended after the current members) as a public static property. + var properties = enumType.Properties; + Assert.AreEqual(3, properties.Count); + Assert.AreEqual("Default", properties[0].Name); + Assert.AreEqual("Recover", properties[1].Name); + Assert.AreEqual("Third", properties[2].Name); + + // The corresponding enum value carries the wire value recovered from the last contract. + var thirdMember = enumType.EnumValues.SingleOrDefault(v => v.Name == "Third"); + Assert.IsNotNull(thirdMember); + Assert.AreEqual("third", thirdMember!.Value); + + // Validate the full generated output, including the restored const `Value` field + // and the preserved backing `_value` field. + var content = new TypeProviderWriter(enumType).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // Validates that a removed extensible enum member is NOT re-added when its removal is accepted in + // the ApiCompat baseline (recorded as a MembersMustExist suppression), so the generator honors the + // intentional removal instead of resurrecting it. Runs against both the text and XML baseline + // formats to ensure either representation of the accepted removal is honored. + [TestCase(".txt")] + [TestCase(".xml")] + public async Task BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts(string baselineExtension) + { + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: baselineExtension); + + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(string), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + apiCompatBaseline: baseline); + + // Last contract: Default, Recover, Third. Current input removes "Third", but the baseline + // accepts that removal, so it must NOT be re-added. + var input = InputFactory.StringEnum("mockInputEnum", [ + ("Default", "default"), + ("Recover", "recover"), + ], isExtensible: true); + + var enumType = EnumProvider.Create(input); + Assert.IsFalse(enumType is ApiVersionEnumProvider); + + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + var properties = enumType.Properties; + Assert.AreEqual(2, properties.Count); + Assert.IsFalse(properties.Any(p => p.Name == "Third")); + Assert.AreEqual("Default", properties[0].Name); + Assert.AreEqual("Recover", properties[1].Name); + Assert.IsFalse(enumType.Fields.Any(f => f.Name == "ThirdValue")); + + // Validate the full generated output; "Third" (property and its const `ThirdValue` field) + // must be absent regardless of which baseline format recorded the accepted removal. + var content = new TypeProviderWriter(enumType).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // Validates that when custom code already provides a member that the current spec removed (and + // that the last contract still declares), back-compat does NOT re-add it. The custom code is left + // as the single source of truth for that member so the generated member does not collide with it. + [Test] + public async Task BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode() + { + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(string), + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last")); + + // Last contract: Default, Recover, Third. Current spec removes "Third", but custom code + // provides it, so back-compat must NOT re-add "Third". + var input = InputFactory.StringEnum("mockInputEnum", [ + ("Default", "default"), + ("Recover", "recover"), + ], isExtensible: true); + + var enumType = EnumProvider.Create(input); + Assert.IsFalse(enumType is ApiVersionEnumProvider); + Assert.IsNotNull(enumType.CustomCodeView); + Assert.IsTrue(enumType.CustomCodeView!.Properties.Any(p => p.Name == "Third")); + + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + // The generated provider must not re-add the custom-owned "Third" member (property or const + // `ThirdValue` field); only the two current members remain in generated code. + var properties = enumType.Properties; + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("Default", properties[0].Name); + Assert.AreEqual("Recover", properties[1].Name); + Assert.IsFalse(properties.Any(p => p.Name == "Third")); + Assert.IsFalse(enumType.Fields.Any(f => f.Name == "ThirdValue")); + + // Validate the full generated output as well. + var content = new TypeProviderWriter(enumType).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // Validates that when custom code re-declares a member that both the current spec and the last + // contract still contain, back-compat leaves the member to the custom code (no duplicate generated + // member) while other removed last-contract members are still restored. + [Test] + public async Task BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored() + { + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(string), + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last")); + + // Last contract: Default, Recover, Third. Current spec keeps Default (customized) and Recover + // but removes "Third". Custom code owns "Default", so it must not be regenerated, while the + // removed "Third" is restored from the last contract. + var input = InputFactory.StringEnum("mockInputEnum", [ + ("Default", "default"), + ("Recover", "recover"), + ], isExtensible: true); + + var enumType = EnumProvider.Create(input); + Assert.IsFalse(enumType is ApiVersionEnumProvider); + Assert.IsNotNull(enumType.CustomCodeView); + Assert.IsTrue(enumType.CustomCodeView!.Properties.Any(p => p.Name == "Default")); + + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + var properties = enumType.Properties; + // "Default" is owned by custom code so it is filtered out of generated code; "Recover" stays + // and "Third" is restored from the last contract, appended after the current members. + Assert.IsFalse(properties.Any(p => p.Name == "Default")); + Assert.IsTrue(properties.Any(p => p.Name == "Recover")); + Assert.IsTrue(properties.Any(p => p.Name == "Third")); + + var thirdMember = enumType.EnumValues.SingleOrDefault(v => v.Name == "Third"); + Assert.IsNotNull(thirdMember); + Assert.AreEqual("third", thirdMember!.Value); + Assert.IsTrue(enumType.Fields.Any(f => f.Name == "ThirdValue")); + } + // Validates that a removed integer enum member is NOT re-added when its removal is accepted // in the ApiCompat baseline (here recorded as an EnumValuesMustMatch suppression), so the // generator honors the intentional removal instead of resurrecting it. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Custom)/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Custom)/MockInputEnum.cs new file mode 100644 index 00000000000..9044df45e5b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Custom)/MockInputEnum.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum + { + public static MockInputEnum Default { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Last)/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Last)/MockInputEnum.cs new file mode 100644 index 00000000000..aa76d3215f1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Last)/MockInputEnum.cs @@ -0,0 +1,27 @@ +#nullable disable + +using System; + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum : IEquatable + { + private readonly string _value; + private const string DefaultValue = "default"; + private const string RecoverValue = "recover"; + private const string ThirdValue = "third"; + + public MockInputEnum(string value) + { + _value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public static MockInputEnum Default { get; } = new MockInputEnum(DefaultValue); + + public static MockInputEnum Recover { get; } = new MockInputEnum(RecoverValue); + + public static MockInputEnum Third { get; } = new MockInputEnum(ThirdValue); + + public bool Equals(MockInputEnum other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.cs new file mode 100644 index 00000000000..da774fa2930 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.cs @@ -0,0 +1,46 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using Sample; + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum : global::System.IEquatable + { + private readonly string _value; + private const string DefaultValue = "default"; + private const string RecoverValue = "recover"; + + public MockInputEnum(string value) + { + global::Sample.Argument.AssertNotNull(value, nameof(value)); + + _value = value; + } + + public static global::Sample.Models.MockInputEnum Default { get; } = new global::Sample.Models.MockInputEnum(DefaultValue); + + public static global::Sample.Models.MockInputEnum Recover { get; } = new global::Sample.Models.MockInputEnum(RecoverValue); + + public static bool operator ==(global::Sample.Models.MockInputEnum left, global::Sample.Models.MockInputEnum right) => left.Equals(right); + + public static bool operator !=(global::Sample.Models.MockInputEnum left, global::Sample.Models.MockInputEnum right) => !left.Equals(right); + + public static implicit operator global::Sample.Models.MockInputEnum(string value) => new global::Sample.Models.MockInputEnum(value); + + public static implicit operator global::Sample.Models.MockInputEnum?(string value) => (value == null) ? null : new global::Sample.Models.MockInputEnum(value); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) => ((obj is global::Sample.Models.MockInputEnum other) && this.Equals(other)); + + public bool Equals(global::Sample.Models.MockInputEnum other) => string.Equals(_value, other._value, global::System.StringComparison.InvariantCultureIgnoreCase); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() => (_value != null) ? global::System.StringComparer.InvariantCultureIgnoreCase.GetHashCode(_value) : 0; + + public override string ToString() => _value; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.txt b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.txt new file mode 100644 index 00000000000..5d107c84471 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.txt @@ -0,0 +1,4 @@ +# The Third extensible-enum member was intentionally removed during migration; suppress the difference +# so the back-compat system honors the removal instead of re-adding it. ApiCompat reports a removed +# extensible-enum member (a public static property) as a MembersMustExist difference on its getter. +MembersMustExist : Member 'public static Sample.Models.MockInputEnum Sample.Models.MockInputEnum.Third.get()' does not exist in the implementation but it does exist in the contract. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.xml new file mode 100644 index 00000000000..e1a45af9549 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.xml @@ -0,0 +1,12 @@ + + + + + CP0002 + M:Sample.Models.MockInputEnum.get_Third + + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts/MockInputEnum.cs new file mode 100644 index 00000000000..aa76d3215f1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts/MockInputEnum.cs @@ -0,0 +1,27 @@ +#nullable disable + +using System; + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum : IEquatable + { + private readonly string _value; + private const string DefaultValue = "default"; + private const string RecoverValue = "recover"; + private const string ThirdValue = "third"; + + public MockInputEnum(string value) + { + _value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public static MockInputEnum Default { get; } = new MockInputEnum(DefaultValue); + + public static MockInputEnum Recover { get; } = new MockInputEnum(RecoverValue); + + public static MockInputEnum Third { get; } = new MockInputEnum(ThirdValue); + + public bool Equals(MockInputEnum other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Custom)/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Custom)/MockInputEnum.cs new file mode 100644 index 00000000000..affc8ad51a8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Custom)/MockInputEnum.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum + { + public static MockInputEnum Third { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Last)/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Last)/MockInputEnum.cs new file mode 100644 index 00000000000..aa76d3215f1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Last)/MockInputEnum.cs @@ -0,0 +1,27 @@ +#nullable disable + +using System; + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum : IEquatable + { + private readonly string _value; + private const string DefaultValue = "default"; + private const string RecoverValue = "recover"; + private const string ThirdValue = "third"; + + public MockInputEnum(string value) + { + _value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public static MockInputEnum Default { get; } = new MockInputEnum(DefaultValue); + + public static MockInputEnum Recover { get; } = new MockInputEnum(RecoverValue); + + public static MockInputEnum Third { get; } = new MockInputEnum(ThirdValue); + + public bool Equals(MockInputEnum other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode.cs new file mode 100644 index 00000000000..da774fa2930 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode.cs @@ -0,0 +1,46 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using Sample; + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum : global::System.IEquatable + { + private readonly string _value; + private const string DefaultValue = "default"; + private const string RecoverValue = "recover"; + + public MockInputEnum(string value) + { + global::Sample.Argument.AssertNotNull(value, nameof(value)); + + _value = value; + } + + public static global::Sample.Models.MockInputEnum Default { get; } = new global::Sample.Models.MockInputEnum(DefaultValue); + + public static global::Sample.Models.MockInputEnum Recover { get; } = new global::Sample.Models.MockInputEnum(RecoverValue); + + public static bool operator ==(global::Sample.Models.MockInputEnum left, global::Sample.Models.MockInputEnum right) => left.Equals(right); + + public static bool operator !=(global::Sample.Models.MockInputEnum left, global::Sample.Models.MockInputEnum right) => !left.Equals(right); + + public static implicit operator global::Sample.Models.MockInputEnum(string value) => new global::Sample.Models.MockInputEnum(value); + + public static implicit operator global::Sample.Models.MockInputEnum?(string value) => (value == null) ? null : new global::Sample.Models.MockInputEnum(value); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) => ((obj is global::Sample.Models.MockInputEnum other) && this.Equals(other)); + + public bool Equals(global::Sample.Models.MockInputEnum other) => string.Equals(_value, other._value, global::System.StringComparison.InvariantCultureIgnoreCase); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() => (_value != null) ? global::System.StringComparer.InvariantCultureIgnoreCase.GetHashCode(_value) : 0; + + public override string ToString() => _value; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded.cs new file mode 100644 index 00000000000..bee3d70db2e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded.cs @@ -0,0 +1,49 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using Sample; + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum : global::System.IEquatable + { + private readonly string _value; + private const string DefaultValue = "default"; + private const string RecoverValue = "recover"; + private const string ThirdValue = "third"; + + public MockInputEnum(string value) + { + global::Sample.Argument.AssertNotNull(value, nameof(value)); + + _value = value; + } + + public static global::Sample.Models.MockInputEnum Default { get; } = new global::Sample.Models.MockInputEnum(DefaultValue); + + public static global::Sample.Models.MockInputEnum Recover { get; } = new global::Sample.Models.MockInputEnum(RecoverValue); + + public static global::Sample.Models.MockInputEnum Third { get; } = new global::Sample.Models.MockInputEnum(ThirdValue); + + public static bool operator ==(global::Sample.Models.MockInputEnum left, global::Sample.Models.MockInputEnum right) => left.Equals(right); + + public static bool operator !=(global::Sample.Models.MockInputEnum left, global::Sample.Models.MockInputEnum right) => !left.Equals(right); + + public static implicit operator global::Sample.Models.MockInputEnum(string value) => new global::Sample.Models.MockInputEnum(value); + + public static implicit operator global::Sample.Models.MockInputEnum?(string value) => (value == null) ? null : new global::Sample.Models.MockInputEnum(value); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) => ((obj is global::Sample.Models.MockInputEnum other) && this.Equals(other)); + + public bool Equals(global::Sample.Models.MockInputEnum other) => string.Equals(_value, other._value, global::System.StringComparison.InvariantCultureIgnoreCase); + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() => (_value != null) ? global::System.StringComparer.InvariantCultureIgnoreCase.GetHashCode(_value) : 0; + + public override string ToString() => _value; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded/MockInputEnum.cs new file mode 100644 index 00000000000..aa76d3215f1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded/MockInputEnum.cs @@ -0,0 +1,27 @@ +#nullable disable + +using System; + +namespace Sample.Models +{ + public readonly partial struct MockInputEnum : IEquatable + { + private readonly string _value; + private const string DefaultValue = "default"; + private const string RecoverValue = "recover"; + private const string ThirdValue = "third"; + + public MockInputEnum(string value) + { + _value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public static MockInputEnum Default { get; } = new MockInputEnum(DefaultValue); + + public static MockInputEnum Recover { get; } = new MockInputEnum(RecoverValue); + + public static MockInputEnum Third { get; } = new MockInputEnum(ThirdValue); + + public bool Equals(MockInputEnum other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs index 4a4b4f24474..c2e0f20be3f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs @@ -645,6 +645,67 @@ public void ValidateEnumFieldsWithExplicitValues() Assert.AreEqual(3, v74Literal!.Literal); } + // Validates that the constant value of a const field on a non-enum type (here a struct, + // mirroring the private `Value` backing constants an extensible enum uses) is + // recovered as the field's initialization value. This is what lets back-compat processing + // read a previously shipped member's wire value from the last contract's metadata. + [Test] + public void ValidateConstFieldInitializerIsRecovered() + { + var compilation = CSharpCompilation.Create( + "Customization", + [CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + public readonly partial struct MockInputEnum + { + private const string RecoverValue = "recover"; + private const int Answer = 42; + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var symbol = compilation.GetTypeByMetadataName("Sample.Models.MockInputEnum"); + Assert.IsNotNull(symbol); + + var provider = new NamedTypeSymbolProvider(symbol!, compilation); + var fields = provider.Fields.ToDictionary(f => f.Name); + + Assert.IsTrue(fields.ContainsKey("RecoverValue")); + var recoverValue = fields["RecoverValue"]; + Assert.IsInstanceOf(recoverValue.InitializationValue); + Assert.AreEqual("recover", (recoverValue.InitializationValue as LiteralExpression)!.Literal); + + Assert.IsTrue(fields.ContainsKey("Answer")); + var answer = fields["Answer"]; + Assert.IsInstanceOf(answer.InitializationValue); + Assert.AreEqual(42, (answer.InitializationValue as LiteralExpression)!.Literal); + } + + // Validates that a non-const field carries no recovered initialization value. + [Test] + public void ValidateNonConstFieldHasNoInitializer() + { + var compilation = CSharpCompilation.Create( + "Customization", + [CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + public readonly partial struct MockInputEnum + { + private readonly string _value; + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var symbol = compilation.GetTypeByMetadataName("Sample.Models.MockInputEnum"); + Assert.IsNotNull(symbol); + + var provider = new NamedTypeSymbolProvider(symbol!, compilation); + var field = provider.Fields.Single(f => f.Name == "_value"); + Assert.IsNull(field.InitializationValue); + } + public enum SomeEnum { Foo, diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 6ca83258649..15d6dd0e768 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -16,6 +16,8 @@ - [Explicit (Non-contiguous) Values Preserved](#scenario-explicit-non-contiguous-values-preserved) - [Removed Integer Enum Member Re-added](#scenario-removed-integer-enum-member-re-added) - [Baseline-Accepted Removal Honored](#scenario-baseline-accepted-removal-honored) + - [Extensible Enum Members](#extensible-enum-members) + - [Removed Extensible Enum Member Re-added](#scenario-removed-extensible-enum-member-re-added) - [API Version Enum](#api-version-enum) - [Non-abstract Base Models](#non-abstract-base-models) - [Model Constructors](#model-constructors) @@ -447,6 +449,53 @@ public enum SampleEnum - Suppressed members are matched by the declaring enum's fully-qualified name and the member name - This lets a library intentionally drop a previously shipped enum member once the removal is reviewed and recorded in the baseline +### Extensible Enum Members + +Extensible enums (C# `readonly partial struct` types) preserve their previously shipped members by comparing the current spec against the last contract. The generator re-adds an extensible enum member that was dropped from the current spec, restoring it with its original wire value to avoid removing a previously shipped member. + +#### Scenario: Removed Extensible Enum Member Re-added + +**Description:** When an extensible enum member present in the last contract is dropped from the current spec, the generator restores it — as a public static property backed by its recovered `const` wire value — to keep the previously shipped API. + +**Example:** + +Previous version: + +```csharp +public readonly partial struct OperationStatusType : IEquatable +{ + private const string CompletedValue = "Completed"; + private const string FailedValue = "Failed"; + private const string RunningValue = "Running"; + + public static OperationStatusType Completed { get; } = new OperationStatusType(CompletedValue); + public static OperationStatusType Failed { get; } = new OperationStatusType(FailedValue); + public static OperationStatusType Running { get; } = new OperationStatusType(RunningValue); + // ... +} +``` + +Current TypeSpec removes `Running`. **Generated Result:** `Running` is re-added (appended after the current members) with its original wire value: + +```csharp +public readonly partial struct OperationStatusType : IEquatable +{ + private const string CompletedValue = "Completed"; + private const string FailedValue = "Failed"; + private const string RunningValue = "Running"; + + public static OperationStatusType Completed { get; } = new OperationStatusType(CompletedValue); + public static OperationStatusType Failed { get; } = new OperationStatusType(FailedValue); + public static OperationStatusType Running { get; } = new OperationStatusType(RunningValue); + // ... +} +``` + +**Key Points:** + +- Members that already exist in the current spec, are provided by custom code, or whose removal is accepted in the [ApiCompat baseline](#apicompat-baseline-awareness) are not re-added +- Restored members are appended after the current spec's members, preserving the current spec's order + ### API Version Enum Service version enums maintain backward compatibility by preserving version values from previous releases.