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.