diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs index 258c5c4d125..aa3d5adcede 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; using Microsoft.TypeSpec.Generator.Expressions; @@ -89,6 +90,44 @@ protected override IReadOnlyList BuildEnumValues() return values; } + protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) + { + var lastContractFields = LastContractView?.Fields; + if (lastContractFields == null || lastContractFields.Count == 0) + { + return null; + } + + var currentLookup = currentValues.ToDictionary(v => v.Name, StringComparer.OrdinalIgnoreCase); + var allMembers = new List(currentValues.Count); + + foreach (var field in lastContractFields) + { + if (currentLookup.TryGetValue(field.Name, out var existingMember)) + { + var updatedField = new FieldProvider( + existingMember.Field.Modifiers, + existingMember.Field.Type, + existingMember.Name, + existingMember.Field.EnclosingType, + existingMember.Field.Description); + allMembers.Add(new EnumTypeMember(existingMember.Name, updatedField, existingMember.Value)); + } + } + + // Then, add new members that weren't in the last contract (in their original input order) + var processedNames = new HashSet(lastContractFields.Select(f => f.Name), StringComparer.OrdinalIgnoreCase); + foreach (var current in currentValues) + { + if (!processedNames.Contains(current.Name)) + { + allMembers.Add(current); + } + } + + return allMembers; + } + protected internal override FieldProvider[] BuildFields() => EnumValues.Select(v => v.Field).ToArray(); 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 81af4c64fe7..e34c6b33e35 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 @@ -580,17 +580,33 @@ internal void ProcessTypeForBackCompatibility() var hasMethods = LastContractView?.Methods != null && LastContractView.Methods.Count > 0; var hasConstructors = LastContractView?.Constructors != null && LastContractView.Constructors.Count > 0; - if (!hasMethods && !hasConstructors) + IEnumerable? newFields = null; + if (this is EnumProvider) { - return; + var hasFields = LastContractView?.Fields != null && LastContractView.Fields.Count > 0; + if (hasFields) + { + var newEnumValues = BuildEnumValuesForBackCompatibility(EnumValues); + if (newEnumValues != null) + { + _enumValues = newEnumValues; + newFields = newEnumValues.Select(v => v.Field); + } + } } var newMethods = hasMethods ? BuildMethodsForBackCompatibility(Methods) : null; var newConstructors = hasConstructors ? BuildConstructorsForBackCompatibility(Constructors) : null; - Update(methods: newMethods, constructors: newConstructors); + if (newFields != null || newMethods != null || newConstructors != null) + { + Update(fields: newFields, methods: newMethods, constructors: newConstructors); + } } + protected internal virtual IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList originalEnumValues) + => null; + protected internal virtual IReadOnlyList BuildMethodsForBackCompatibility(IEnumerable originalMethods) => [.. originalMethods]; 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 b12df97b8cf..218995d1677 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 @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Linq; +using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Utilities; @@ -364,6 +365,139 @@ public void InternalModelsAreNotIncludedInAdditionalRootTypes() Assert.IsFalse(rootTypes.Contains("Sample.Models.StringEnum")); } + // Validates that int enum member order is preserved from the last contract when values are reordered + [Test] + public async Task BackCompat_IntEnumOrderPreserved() + { + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(int), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // Current input has values in DIFFERENT order than last contract (Default first, Recover second) + var input = InputFactory.Int32Enum("mockInputEnum", [ + ("Default", 0), + ("Recover", 1), + ]); + + var enumType = EnumProvider.Create(input); + Assert.IsFalse(enumType is ApiVersionEnumProvider); + + // Simulate the back-compat processing that CSharpGen performs after visitors + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + var fields = enumType.Fields; + Assert.AreEqual(2, fields.Count); + + // Order should be preserved from last contract: Recover first, Default second + Assert.AreEqual("Recover", fields[0].Name); + Assert.AreEqual("Default", fields[1].Name); + + // No explicit initialization values - compiler auto-assigns based on order + Assert.IsNull(fields[0].InitializationValue); + Assert.IsNull(fields[1].InitializationValue); + } + + // Validates that int enum member order is preserved and new values are appended + [Test] + public async Task BackCompat_IntEnumNewValueAppended() + { + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(int), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // Current input has different order AND a new value + var input = InputFactory.Int32Enum("mockInputEnum", [ + ("Default", 0), + ("Recover", 1), + ("Third", 2), + ]); + + var enumType = EnumProvider.Create(input); + Assert.IsFalse(enumType is ApiVersionEnumProvider); + + // Simulate the back-compat processing that CSharpGen performs after visitors + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + var fields = enumType.Fields; + Assert.AreEqual(3, fields.Count); + + // Order should be preserved from last contract: Recover first, Default second, new value Third appended + Assert.AreEqual("Recover", fields[0].Name); + Assert.AreEqual("Default", fields[1].Name); + Assert.AreEqual("Third", fields[2].Name); + + // No explicit initialization values for reordered members - compiler auto-assigns based on order + Assert.IsNull(fields[0].InitializationValue); + Assert.IsNull(fields[1].InitializationValue); + // New value keeps its initialization value from the input + var value3 = fields[2].InitializationValue as LiteralExpression; + Assert.IsNotNull(value3); + Assert.AreEqual(2, value3?.Literal); + } + + // Validates that removed enum values from last contract are not included + [Test] + public async Task BackCompat_IntEnumValueRemoved() + { + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(int), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // Current input has values in different order and removed "Third" + var input = InputFactory.Int32Enum("mockInputEnum", [ + ("Default", 0), + ("Recover", 1), + ]); + + var enumType = EnumProvider.Create(input); + Assert.IsFalse(enumType is ApiVersionEnumProvider); + + // Simulate the back-compat processing that CSharpGen performs after visitors + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + var fields = enumType.Fields; + Assert.AreEqual(2, fields.Count); + + // Order should be preserved from last contract for members that still exist + Assert.AreEqual("Recover", fields[0].Name); + Assert.AreEqual("Default", fields[1].Name); + + // No explicit initialization values - compiler auto-assigns based on order + Assert.IsNull(fields[0].InitializationValue); + Assert.IsNull(fields[1].InitializationValue); + } + + // Validates that string enum order is also preserved from last contract + [Test] + public async Task BackCompat_StringEnumOrderPreserved() + { + await MockHelpers.LoadMockGeneratorAsync( + createCSharpTypeCore: (inputType) => typeof(string), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // Current input has values in DIFFERENT order than last contract + var input = InputFactory.StringEnum("mockInputEnum", [ + ("Default", "default"), + ("Recover", "recover"), + ]); + + var enumType = EnumProvider.Create(input); + + // Simulate the back-compat processing that CSharpGen performs after visitors + enumType.EnsureBuilt(); + enumType.ProcessTypeForBackCompatibility(); + + var fields = enumType.Fields; + Assert.AreEqual(2, fields.Count); + + // Order should be preserved from last contract: Recover first, Default second + Assert.AreEqual("Recover", fields[0].Name); + Assert.AreEqual("Default", fields[1].Name); + } + private static void ValidateGetHashCodeMethod(EnumProvider enumType) { var getHashCodeMethod = enumType.Methods.Single(m => m.Signature.Name == "GetHashCode"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumNewValueAppended/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumNewValueAppended/MockInputEnum.cs new file mode 100644 index 00000000000..1c6a58d2c47 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumNewValueAppended/MockInputEnum.cs @@ -0,0 +1,10 @@ +#nullable disable + +namespace Sample.Models +{ + public enum MockInputEnum + { + Recover = 0, + Default = 1, + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumOrderPreserved/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumOrderPreserved/MockInputEnum.cs new file mode 100644 index 00000000000..1c6a58d2c47 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumOrderPreserved/MockInputEnum.cs @@ -0,0 +1,10 @@ +#nullable disable + +namespace Sample.Models +{ + public enum MockInputEnum + { + Recover = 0, + Default = 1, + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumValueRemoved/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumValueRemoved/MockInputEnum.cs new file mode 100644 index 00000000000..e93e6f85a4a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_IntEnumValueRemoved/MockInputEnum.cs @@ -0,0 +1,11 @@ +#nullable disable + +namespace Sample.Models +{ + public enum MockInputEnum + { + Recover = 0, + Default = 1, + Third = 2, + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_StringEnumOrderPreserved/MockInputEnum.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_StringEnumOrderPreserved/MockInputEnum.cs new file mode 100644 index 00000000000..eefcc51dd97 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompat_StringEnumOrderPreserved/MockInputEnum.cs @@ -0,0 +1,10 @@ +#nullable disable + +namespace Sample.Models +{ + public enum MockInputEnum + { + Recover, + Default, + } +}