From c41223b6d2241995f9c098c3091326b1b907dade Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 30 Jul 2026 22:04:31 +0000
Subject: [PATCH 1/6] Initial plan
From ccc0595f1451b649308a857e3164c8a0b20a26a3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 30 Jul 2026 22:37:20 +0000
Subject: [PATCH 2/6] Restore removed extensible enum members via back compat
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
---
.../src/LibraryVisitor.cs | 2 +-
.../src/Providers/EnumProvider.cs | 2 +-
.../src/Providers/ExtensibleEnumProvider.cs | 83 +++++++++++++++++++
.../src/Providers/NamedTypeSymbolProvider.cs | 12 +--
.../src/Providers/TypeProvider.cs | 39 +++++++--
.../test/OutputLibraryVisitorTests.cs | 2 +-
.../EnumProviders/EnumProviderTests.cs | 78 +++++++++++++++++
...ovedValueNotReaddedWhenBaselineAccepts.txt | 4 +
.../MockInputEnum.cs | 27 ++++++
.../MockInputEnum.cs | 27 ++++++
.../generator/docs/backward-compatibility.md | 53 ++++++++++++
11 files changed, 311 insertions(+), 18 deletions(-)
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.txt
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts/MockInputEnum.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded/MockInputEnum.cs
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/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..c801192f5dd 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,86 @@ protected override TypeProvider[] BuildSerializationProviders()
return CodeModelGenerator.Instance.TypeFactory.CreateSerializations(_inputType, this).ToArray();
}
protected override bool GetIsEnum() => true;
+
+ protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues)
+ {
+ // Extensible enum members surface as public static properties whose wire values are stored in
+ // private const `Value` fields. Both the property names and the const values are
+ // recoverable from the last contract (including a compiled assembly's metadata), so a member
+ // dropped from the current spec can be restored to avoid a source-breaking removal.
+ 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[field.Name] = field;
+ }
+
+ List? readdedMembers = 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))
+ {
+ (readdedMembers ??= []).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 (readdedMembers == null)
+ {
+ return null;
+ }
+
+ // Preserve the current spec order and append the restored members at the end.
+ return [.. currentValues, .. readdedMembers];
+ }
+
+ 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,
+ lastContractProperty.Description,
+ 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..9dd63f9f61a 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
@@ -249,13 +249,13 @@ protected internal override PropertyProvider[] BuildProperties()
private static ValueExpression? GetFieldInitializer(IFieldSymbol fieldSymbol)
{
- if (fieldSymbol.ContainingType?.TypeKind == TypeKind.Enum)
+ // Constant fields expose their value through metadata, so we can recover it even from a
+ // compiled last-contract assembly without debug symbols. This covers both enum members and
+ // the private const backing fields of an extensible enum (e.g. `Value`), which lets
+ // back-compat restore an extensible enum member that was dropped from the current spec.
+ if (fieldSymbol.HasConstantValue && fieldSymbol.ConstantValue != null)
{
- if (fieldSymbol.HasConstantValue && fieldSymbol.ConstantValue != null)
- {
- return Literal(fieldSymbol.ConstantValue);
- }
- return null;
+ return Literal(fieldSymbol.ConstantValue);
}
return null;
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..7d028c7de62 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,28 @@ internal void ProcessTypeForBackCompatibility()
updatedEnumValues = newEnumValues;
}
- newFields = filteredFields;
+ // Sync the enum values before rebuilding the member collections from them.
+ _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 property instances for members that still exist so only the
+ // restored members are (re)visited below.
+ newFields = ApplyCustomizationFilter(BuildFields());
+ var existingProperties = new Dictionary(StringComparer.Ordinal);
+ foreach (var property in Properties)
+ {
+ existingProperties[property.Name] = property;
+ }
+ newProperties = ApplyCustomizationFilter(
+ BuildProperties().Select(p => existingProperties.TryGetValue(p.Name, out var existing) ? existing : p));
+ }
+ else
+ {
+ newFields = filteredFields;
+ }
}
}
}
@@ -856,13 +878,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 +894,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..825bbe24df4 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,84 @@ 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 re-added member restores its wire value from the last contract's private const field.
+ var thirdValueField = enumType.Fields.SingleOrDefault(f => f.Name == "ThirdValue");
+ Assert.IsNotNull(thirdValueField);
+ Assert.AreEqual("third", (thirdValueField!.InitializationValue as LiteralExpression)?.Literal);
+
+ // The backing `_value` field is preserved when the fields are rebuilt.
+ Assert.IsTrue(enumType.Fields.Any(f => f.Name == "_value"));
+
+ // The corresponding enum value carries the recovered wire value.
+ var thirdMember = enumType.EnumValues.SingleOrDefault(v => v.Name == "Third");
+ Assert.IsNotNull(thirdMember);
+ Assert.AreEqual("third", thirdMember!.Value);
+ }
+
+ // 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.
+ [Test]
+ public async Task BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts()
+ {
+ var baseline = Helpers.GetApiCompatBaselineFromFile();
+
+ 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"));
+ }
+
// 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_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/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_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/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md
index 6ca83258649..10bd96cf26f 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,57 @@ 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 are generated as `readonly partial struct` types whose members are exposed as public static properties, with each member's wire value stored in a private `const` backing field (`Value`). Because both the property name and its wire value are recoverable from the last contract — including from a compiled assembly's metadata — the generator can re-add an extensible enum member that was dropped from the current spec, avoiding a source-breaking removal.
+
+> [!NOTE]
+> Unlike fixed **string** enums (where the wire value is not recoverable from the last contract), extensible enums keep the wire value in a `const` field, so removed members can be safely restored.
+
+#### 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:**
+
+- The member's property name and its wire value are read from the previously published assembly's metadata (no debug symbols required)
+- 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.
From 1ab538b590bae625c122fa63900c91f9c28792fd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 31 Jul 2026 16:06:46 +0000
Subject: [PATCH 3/6] Address PR review feedback on extensible enum back-compat
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
---
.../src/Providers/ExtensibleEnumProvider.cs | 6 +--
.../src/Providers/NamedTypeSymbolProvider.cs | 4 --
.../src/Providers/TypeProvider.cs | 14 ++++--
.../EnumProviders/EnumProviderTests.cs | 15 +++---
...ompat_ExtensibleEnumRemovedValueReadded.cs | 49 +++++++++++++++++++
.../generator/docs/backward-compatibility.md | 5 +-
6 files changed, 68 insertions(+), 25 deletions(-)
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueReadded.cs
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 c801192f5dd..61b65a6a91b 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
@@ -279,10 +279,6 @@ protected override TypeProvider[] BuildSerializationProviders()
protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues)
{
- // Extensible enum members surface as public static properties whose wire values are stored in
- // private const `Value` fields. Both the property names and the const values are
- // recoverable from the last contract (including a compiled assembly's metadata), so a member
- // dropped from the current spec can be restored to avoid a source-breaking removal.
var lastContractProperties = LastContractView?.Properties;
if (lastContractProperties == null || lastContractProperties.Count == 0)
{
@@ -293,7 +289,7 @@ protected override TypeProvider[] BuildSerializationProviders()
var lastContractValueFields = new Dictionary(StringComparer.Ordinal);
foreach (var field in LastContractView!.Fields)
{
- lastContractValueFields[field.Name] = field;
+ lastContractValueFields.TryAdd(field.Name, field);
}
List? readdedMembers = null;
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 9dd63f9f61a..d8be8a10706 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
@@ -249,10 +249,6 @@ protected internal override PropertyProvider[] BuildProperties()
private static ValueExpression? GetFieldInitializer(IFieldSymbol fieldSymbol)
{
- // Constant fields expose their value through metadata, so we can recover it even from a
- // compiled last-contract assembly without debug symbols. This covers both enum members and
- // the private const backing fields of an extensible enum (e.g. `Value`), which lets
- // back-compat restore an extensible enum member that was dropped from the current spec.
if (fieldSymbol.HasConstantValue && fieldSymbol.ConstantValue != null)
{
return Literal(fieldSymbol.ConstantValue);
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 7d028c7de62..843176b5af5 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
@@ -856,9 +856,17 @@ internal void ProcessTypeForBackCompatibility()
{
// Extensible enums carry an extra backing `_value` field and surface members
// as properties, so rebuild both from the updated members. Reuse the
- // already-visited property instances for members that still exist so only the
- // restored members are (re)visited below.
- newFields = ApplyCustomizationFilter(BuildFields());
+ // 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[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)
{
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 825bbe24df4..99de806e2a1 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
@@ -680,18 +680,15 @@ await MockHelpers.LoadMockGeneratorAsync(
Assert.AreEqual("Recover", properties[1].Name);
Assert.AreEqual("Third", properties[2].Name);
- // The re-added member restores its wire value from the last contract's private const field.
- var thirdValueField = enumType.Fields.SingleOrDefault(f => f.Name == "ThirdValue");
- Assert.IsNotNull(thirdValueField);
- Assert.AreEqual("third", (thirdValueField!.InitializationValue as LiteralExpression)?.Literal);
-
- // The backing `_value` field is preserved when the fields are rebuilt.
- Assert.IsTrue(enumType.Fields.Any(f => f.Name == "_value"));
-
- // The corresponding enum value carries the recovered wire value.
+ // 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
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/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md
index 10bd96cf26f..bf71e1cd856 100644
--- a/packages/http-client-csharp/generator/docs/backward-compatibility.md
+++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md
@@ -451,10 +451,7 @@ public enum SampleEnum
### Extensible Enum Members
-Extensible enums are generated as `readonly partial struct` types whose members are exposed as public static properties, with each member's wire value stored in a private `const` backing field (`Value`). Because both the property name and its wire value are recoverable from the last contract — including from a compiled assembly's metadata — the generator can re-add an extensible enum member that was dropped from the current spec, avoiding a source-breaking removal.
-
-> [!NOTE]
-> Unlike fixed **string** enums (where the wire value is not recoverable from the last contract), extensible enums keep the wire value in a `const` field, so removed members can be safely restored.
+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
From 0ae951a12f82da4ad8be45206fed425d39de127d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 31 Jul 2026 17:02:14 +0000
Subject: [PATCH 4/6] Address PR review: TryAdd, guard, spell fixes,
custom-code & symbol tests
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
---
.../src/Providers/ExtensibleEnumProvider.cs | 8 +-
.../src/Providers/TypeProvider.cs | 9 +-
.../EnumProviders/EnumProviderTests.cs | 95 ++++++++++++++++++-
.../MockInputEnum.cs | 9 ++
.../MockInputEnum.cs | 27 ++++++
...movedValueNotReaddedWhenBaselineAccepts.cs | 46 +++++++++
...ovedValueNotReaddedWhenBaselineAccepts.xml | 12 +++
.../MockInputEnum.cs | 9 ++
.../MockInputEnum.cs | 27 ++++++
...ValueNotReaddedWhenProvidedByCustomCode.cs | 46 +++++++++
.../NamedTypeSymbolProviderTests.cs | 61 ++++++++++++
.../generator/docs/backward-compatibility.md | 1 -
12 files changed, 338 insertions(+), 12 deletions(-)
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Custom)/MockInputEnum.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumCustomCodeMemberPreservedWhileOtherMemberRestored(Last)/MockInputEnum.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts.xml
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Custom)/MockInputEnum.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode(Last)/MockInputEnum.cs
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenProvidedByCustomCode.cs
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 61b65a6a91b..bacc3912c58 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
@@ -292,7 +292,7 @@ protected override TypeProvider[] BuildSerializationProviders()
lastContractValueFields.TryAdd(field.Name, field);
}
- List? readdedMembers = null;
+ 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.
@@ -312,20 +312,20 @@ protected override TypeProvider[] BuildSerializationProviders()
if (TryResurrectRemovedMember(property, lastContractValueFields, out var resurrectedMember))
{
- (readdedMembers ??= []).Add(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 (readdedMembers == null)
+ if (restoredMembers == null)
{
return null;
}
// Preserve the current spec order and append the restored members at the end.
- return [.. currentValues, .. readdedMembers];
+ return [.. currentValues, .. restoredMembers];
}
private bool TryResurrectRemovedMember(
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 843176b5af5..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
@@ -850,7 +850,10 @@ internal void ProcessTypeForBackCompatibility()
}
// Sync the enum values before rebuilding the member collections from them.
- _enumValues = updatedEnumValues;
+ if (updatedEnumValues != null)
+ {
+ _enumValues = updatedEnumValues;
+ }
if (enumProvider.IsExtensible)
{
@@ -862,7 +865,7 @@ internal void ProcessTypeForBackCompatibility()
var existingFields = new Dictionary(StringComparer.Ordinal);
foreach (var field in Fields)
{
- existingFields[field.Name] = field;
+ existingFields.TryAdd(field.Name, field);
}
newFields = ApplyCustomizationFilter(
BuildFields().Select(f => existingFields.TryGetValue(f.Name, out var existing) ? existing : f));
@@ -870,7 +873,7 @@ internal void ProcessTypeForBackCompatibility()
var existingProperties = new Dictionary(StringComparer.Ordinal);
foreach (var property in Properties)
{
- existingProperties[property.Name] = property;
+ existingProperties.TryAdd(property.Name, property);
}
newProperties = ApplyCustomizationFilter(
BuildProperties().Select(p => existingProperties.TryGetValue(p.Name, out var existing) ? existing : p));
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 99de806e2a1..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
@@ -693,11 +693,13 @@ await MockHelpers.LoadMockGeneratorAsync(
// 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.
- [Test]
- public async Task BackCompat_ExtensibleEnumRemovedValueNotReaddedWhenBaselineAccepts()
+ // 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();
+ var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: baselineExtension);
await MockHelpers.LoadMockGeneratorAsync(
createCSharpTypeCore: (inputType) => typeof(string),
@@ -723,6 +725,91 @@ await MockHelpers.LoadMockGeneratorAsync(
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
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.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_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/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 bf71e1cd856..15d6dd0e768 100644
--- a/packages/http-client-csharp/generator/docs/backward-compatibility.md
+++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md
@@ -493,7 +493,6 @@ public readonly partial struct OperationStatusType : IEquatable
Date: Fri, 31 Jul 2026 19:49:11 +0000
Subject: [PATCH 5/6] fix(csharp-generator): recover null const field
initializer values
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
---
.../src/Providers/NamedTypeSymbolProvider.cs | 2 +-
.../NamedTypeSymbolProviderTests.cs | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
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 d8be8a10706..7f771583133 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
@@ -249,7 +249,7 @@ protected internal override PropertyProvider[] BuildProperties()
private static ValueExpression? GetFieldInitializer(IFieldSymbol fieldSymbol)
{
- if (fieldSymbol.HasConstantValue && fieldSymbol.ConstantValue != null)
+ if (fieldSymbol.HasConstantValue)
{
return Literal(fieldSymbol.ConstantValue);
}
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 c2e0f20be3f..eac80bc74e0 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
@@ -660,6 +660,7 @@ namespace Sample.Models
public readonly partial struct MockInputEnum
{
private const string RecoverValue = "recover";
+ private const string NullValue = null;
private const int Answer = 42;
}
}
@@ -680,6 +681,11 @@ public readonly partial struct MockInputEnum
var answer = fields["Answer"];
Assert.IsInstanceOf(answer.InitializationValue);
Assert.AreEqual(42, (answer.InitializationValue as LiteralExpression)!.Literal);
+
+ Assert.IsTrue(fields.ContainsKey("NullValue"));
+ var nullValue = fields["NullValue"];
+ Assert.IsInstanceOf(nullValue.InitializationValue);
+ Assert.IsNull((nullValue.InitializationValue as LiteralExpression)!.Literal);
}
// Validates that a non-const field carries no recovered initialization value.
From c5df9d67ad575d614976c6bd0af5b5cef7817cc0 Mon Sep 17 00:00:00 2001
From: Jorge Rangel
Date: Fri, 31 Jul 2026 16:50:56 -0500
Subject: [PATCH 6/6] fix metadata loading
---
.../src/PostProcessing/GeneratedCodeWorkspace.cs | 3 ++-
.../src/Providers/ExtensibleEnumProvider.cs | 3 +--
2 files changed, 3 insertions(+), 3 deletions(-)
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/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs
index bacc3912c58..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
@@ -348,8 +348,7 @@ private bool TryResurrectRemovedMember(
EnumUnderlyingType,
valueFieldName,
this,
- lastContractProperty.Description,
- Literal(literalValue));
+ initializationValue: Literal(literalValue));
member = new EnumTypeMember(lastContractProperty.Name, field, literalValue);
return true;
}