From a7ee15fc631c1147dbc1ab4cf60bf91c50b12616 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 23 Jul 2025 13:54:53 +0800 Subject: [PATCH] Add gRPC JSON transcoding option for stripping enum prefix --- .../GrpcJsonSettings.cs | 38 ++++ .../Internal/Json/EnumConverter.cs | 20 +-- .../Internal/Json/EnumNameHelpers.cs | 164 ++++++++++++++++++ .../PublicAPI.Unshipped.txt | 2 + src/Grpc/JsonTranscoding/src/Shared/Legacy.cs | 41 +---- .../ConverterTests/JsonConverterReadTests.cs | 45 ++++- .../ConverterTests/JsonConverterWriteTests.cs | 87 ++++++++-- .../Proto/transcoding.proto | 20 +++ 8 files changed, 343 insertions(+), 74 deletions(-) create mode 100644 src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumNameHelpers.cs diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs index b18519c76e0a..6a4ab85c5ee5 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/GrpcJsonSettings.cs @@ -48,4 +48,42 @@ public sealed class GrpcJsonSettings /// /// public bool PropertyNameCaseInsensitive { get; set; } + + /// + /// Gets or sets a value indicating whether the enum type name prefix should be removed when reading and writing enum values. + /// The default value is . + /// + /// + /// + /// In Protocol Buffers, enum value names are globally scoped, so they are often prefixed with the enum type name + /// to avoid name collisions. For example, the Status enum might define values like STATUS_UNKNOWN + /// and STATUS_OK. + /// + /// + /// enum Status { + /// STATUS_UNKNOWN = 0; + /// STATUS_OK = 1; + /// } + /// + /// + /// When is set to : + /// + /// + /// + /// The STATUS prefix is removed from enum values. The enum values above will be read and written as UNKNOWN and OK instead of STATUS_UNKNOWN and STATUS_OK. + /// + /// + /// Original prefixed values are used as a fallback when reading JSON. For example, STATUS_OK and OK map to the STATUS_OK enum value. + /// + /// + /// + /// The Protobuf JSON specification requires enum values in JSON to match enum fields exactly. + /// Enabling this option may reduce interoperability, as removing enum prefix might not be supported + /// by other JSON transcoding implementations. + /// + /// + /// For more information, see . + /// + /// + public bool RemoveEnumPrefix { get; set; } } diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumConverter.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumConverter.cs index c2779e931d83..6f761424d571 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumConverter.cs +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumConverter.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Text.Json; using Google.Protobuf.Reflection; -using Grpc.Shared; using Type = System.Type; namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json; @@ -21,18 +20,12 @@ public EnumConverter(JsonContext context) : base(context) switch (reader.TokenType) { case JsonTokenType.String: - var enumDescriptor = (EnumDescriptor?)Context.DescriptorRegistry.FindDescriptorByType(typeToConvert); - if (enumDescriptor == null) - { - throw new InvalidOperationException($"Unable to resolve descriptor for {typeToConvert}."); - } + var enumDescriptor = (EnumDescriptor?)Context.DescriptorRegistry.FindDescriptorByType(typeToConvert) + ?? throw new InvalidOperationException($"Unable to resolve descriptor for {typeToConvert}."); var value = reader.GetString()!; - var valueDescriptor = enumDescriptor.FindValueByName(value); - if (valueDescriptor == null) - { - throw new InvalidOperationException(@$"Error converting value ""{value}"" to enum type {typeToConvert}."); - } + var valueDescriptor = JsonNamingHelpers.GetEnumFieldReadValue(enumDescriptor, value, Context.Settings) + ?? throw new InvalidOperationException(@$"Error converting value ""{value}"" to enum type {typeToConvert}."); return ConvertFromInteger(valueDescriptor.Number); case JsonTokenType.Number: @@ -52,7 +45,10 @@ public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOpt } else { - var name = Legacy.OriginalEnumValueHelper.GetOriginalName(value); + var enumDescriptor = (EnumDescriptor?)Context.DescriptorRegistry.FindDescriptorByType(value.GetType()) + ?? throw new InvalidOperationException($"Unable to resolve descriptor for {value.GetType()}."); + + var name = JsonNamingHelpers.GetEnumFieldWriteName(enumDescriptor, value, Context.Settings); if (name != null) { writer.WriteStringValue(name); diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumNameHelpers.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumNameHelpers.cs new file mode 100644 index 000000000000..986524d4ec74 --- /dev/null +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Json/EnumNameHelpers.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Linq; +using System.Reflection; +using Google.Protobuf.Reflection; + +namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json; + +internal static class JsonNamingHelpers +{ + private static readonly ConcurrentDictionary _enumMappings = new ConcurrentDictionary(); + + internal static EnumValueDescriptor? GetEnumFieldReadValue(EnumDescriptor enumDescriptor, string value, GrpcJsonSettings settings) + { + string resolvedName; + if (settings.RemoveEnumPrefix) + { + var nameMapping = GetEnumMapping(enumDescriptor); + if (!nameMapping.RemoveEnumPrefixMapping.TryGetValue(value, out var n)) + { + return null; + } + + resolvedName = n; + } + else + { + resolvedName = value; + } + + return enumDescriptor.FindValueByName(resolvedName); + } + + internal static string? GetEnumFieldWriteName(EnumDescriptor enumDescriptor, object value, GrpcJsonSettings settings) + { + var enumMapping = GetEnumMapping(enumDescriptor); + + // If this returns false, name will be null, which is what we want. + if (!enumMapping.WriteMapping.TryGetValue(value, out var mapping)) + { + return null; + } + + return settings.RemoveEnumPrefix ? mapping.RemoveEnumPrefixName : mapping.OriginalName; + } + + private static EnumMapping GetEnumMapping(EnumDescriptor enumDescriptor) + { + return _enumMappings.GetOrAdd( + enumDescriptor.ClrType, + static (t, descriptor) => GetEnumMapping(descriptor.Name, t), + enumDescriptor); + } + + private static EnumMapping GetEnumMapping(string enumName, Type enumType) + { + var nameMappings = enumType.GetTypeInfo().DeclaredFields + .Where(f => f.IsStatic) + .Where(f => f.GetCustomAttributes().FirstOrDefault()?.PreferredAlias ?? true) + .Select(f => + { + // If the attribute hasn't been applied, fall back to the name of the field. + var fieldName = f.GetCustomAttributes().FirstOrDefault()?.Name ?? f.Name; + + return new NameMapping + { + Value = f.GetValue(null)!, + OriginalName = fieldName, + RemoveEnumPrefixName = GetEnumValueName(enumName, fieldName) + }; + }) + .ToList(); + + var writeMapping = nameMappings.ToDictionary(m => m.Value, m => m); + + // Add original names as fallback when mapping enum values with removed prefixes. + // There are added to the dictionary first so they are overridden by the mappings with removed prefixes. + var removeEnumPrefixMapping = nameMappings.ToDictionary(m => m.OriginalName, m => m.OriginalName); + + // Protobuf codegen prevents collision of enum names when the prefix is removed. + // For example, the following enum will fail to build because both fields would resolve to "OK": + // + // enum Status { + // STATUS_OK = 0; + // OK = 1; + // } + // + // Tooling error message: + // ---------------------- + // Enum name OK has the same name as STATUS_OK if you ignore case and strip out the enum name prefix (if any). + // (If you are using allow_alias, please assign the same number to each enum value name.) + // + // Just in case it does happen, map to the first value rather than error. + foreach (var item in nameMappings.GroupBy(m => m.RemoveEnumPrefixName).Select(g => KeyValuePair.Create(g.Key, g.First().OriginalName))) + { + removeEnumPrefixMapping[item.Key] = item.Value; + } + + return new EnumMapping { WriteMapping = writeMapping, RemoveEnumPrefixMapping = removeEnumPrefixMapping }; + } + + // Remove the prefix from the specified value. Ignore case and underscores in the comparison. + private static string TryRemovePrefix(string prefix, string value) + { + var normalizedPrefix = prefix.Replace("_", string.Empty, StringComparison.Ordinal).ToLowerInvariant(); + + var prefixIndex = 0; + var valueIndex = 0; + + while (prefixIndex < normalizedPrefix.Length && valueIndex < value.Length) + { + if (value[valueIndex] == '_') + { + valueIndex++; + continue; + } + + if (char.ToLowerInvariant(value[valueIndex]) != normalizedPrefix[prefixIndex]) + { + return value; + } + + prefixIndex++; + valueIndex++; + } + + if (prefixIndex < normalizedPrefix.Length) + { + return value; + } + + while (valueIndex < value.Length && value[valueIndex] == '_') + { + valueIndex++; + } + + return valueIndex == value.Length ? value : value.Substring(valueIndex); + } + + private static string GetEnumValueName(string enumName, string valueName) + { + var result = TryRemovePrefix(enumName, valueName); + + // Prefix name starting with a digit with an underscore to ensure it is a valid identifier. + return result.Length > 0 && char.IsDigit(result[0]) + ? $"_{result}" + : result; + } + + private sealed class EnumMapping + { + public required Dictionary WriteMapping { get; init; } + public required Dictionary RemoveEnumPrefixMapping { get; init; } + } + + private sealed class NameMapping + { + public required object Value { get; init; } + public required string OriginalName { get; init; } + public required string RemoveEnumPrefixName { get; init; } + } +} diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt index 6da56881ef04..b11ea58cf311 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.get -> bool Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.PropertyNameCaseInsensitive.set -> void +Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.RemoveEnumPrefix.get -> bool +Microsoft.AspNetCore.Grpc.JsonTranscoding.GrpcJsonSettings.RemoveEnumPrefix.set -> void diff --git a/src/Grpc/JsonTranscoding/src/Shared/Legacy.cs b/src/Grpc/JsonTranscoding/src/Shared/Legacy.cs index 7cb1304980bc..6de5e8c04805 100644 --- a/src/Grpc/JsonTranscoding/src/Shared/Legacy.cs +++ b/src/Grpc/JsonTranscoding/src/Shared/Legacy.cs @@ -40,6 +40,7 @@ using System.Text.RegularExpressions; using Google.Protobuf.Reflection; using Google.Protobuf.WellKnownTypes; +using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json; using Type = System.Type; namespace Grpc.Shared; @@ -365,44 +366,4 @@ internal static bool IsPathValid(string input) } return true; } - - // Effectively a cache of mapping from enum values to the original name as specified in the proto file, - // fetched by reflection. - // The need for this is unfortunate, as is its unbounded size, but realistically it shouldn't cause issues. - internal static class OriginalEnumValueHelper - { - private static readonly ConcurrentDictionary> _dictionaries - = new ConcurrentDictionary>(); - - internal static string? GetOriginalName(object value) - { - var enumType = value.GetType(); - Dictionary? nameMapping; - lock (_dictionaries) - { - if (!_dictionaries.TryGetValue(enumType, out nameMapping)) - { - nameMapping = GetNameMapping(enumType); - _dictionaries[enumType] = nameMapping; - } - } - - // If this returns false, originalName will be null, which is what we want. - nameMapping.TryGetValue(value, out var originalName); - return originalName; - } - - private static Dictionary GetNameMapping(Type enumType) - { - return enumType.GetTypeInfo().DeclaredFields - .Where(f => f.IsStatic) - .Where(f => f.GetCustomAttributes() - .FirstOrDefault()?.PreferredAlias ?? true) - .ToDictionary(f => f.GetValue(null)!, - f => f.GetCustomAttributes() - .FirstOrDefault() - // If the attribute hasn't been applied, fall back to the name of the field. - ?.Name ?? f.Name); - } - } } diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs index 6a2bf07ca829..7876516e4930 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterReadTests.cs @@ -264,17 +264,52 @@ public void Enum_ReadNumber(int value) } [Theory] - [InlineData("FOO")] - [InlineData("BAR")] - [InlineData("NEG")] - public void Enum_ReadString(string value) + [InlineData("FOO", HelloRequest.Types.DataTypes.Types.NestedEnum.Foo)] + [InlineData("BAR", HelloRequest.Types.DataTypes.Types.NestedEnum.Bar)] + [InlineData("NEG", HelloRequest.Types.DataTypes.Types.NestedEnum.Neg)] + public void Enum_ReadString(string value, HelloRequest.Types.DataTypes.Types.NestedEnum expectedValue) { var serviceDescriptorRegistry = new DescriptorRegistry(); serviceDescriptorRegistry.RegisterFileDescriptor(JsonTranscodingGreeter.Descriptor.File); var json = @$"{{ ""singleEnum"": ""{value}"" }}"; - AssertReadJson(json, descriptorRegistry: serviceDescriptorRegistry); + var result = AssertReadJson(json, descriptorRegistry: serviceDescriptorRegistry); + Assert.Equal(expectedValue, result.SingleEnum); + } + + [Theory] + [InlineData("UNSPECIFIED", PrefixEnumType.Types.PrefixEnum.Unspecified)] + [InlineData("PREFIX_ENUM_UNSPECIFIED", PrefixEnumType.Types.PrefixEnum.Unspecified)] + [InlineData("FOO", PrefixEnumType.Types.PrefixEnum.Foo)] + [InlineData("PREFIX_ENUM_FOO", PrefixEnumType.Types.PrefixEnum.Foo)] + [InlineData("BAR", PrefixEnumType.Types.PrefixEnum.Bar)] + public void Enum_RemovePrefix_ReadString(string value, PrefixEnumType.Types.PrefixEnum expectedValue) + { + var serviceDescriptorRegistry = new DescriptorRegistry(); + serviceDescriptorRegistry.RegisterFileDescriptor(JsonTranscodingGreeter.Descriptor.File); + + var json = @$"{{ ""singleEnum"": ""{value}"" }}"; + + var result = AssertReadJson(json, descriptorRegistry: serviceDescriptorRegistry, serializeOld: false, settings: new GrpcJsonSettings { RemoveEnumPrefix = true }); + Assert.Equal(expectedValue, result.SingleEnum); + } + + [Theory] + [InlineData("UNSPECIFIED", CollisionPrefixEnumType.Types.CollisionPrefixEnum.Unspecified)] + [InlineData("COLLISION_PREFIX_ENUM_UNSPECIFIED", CollisionPrefixEnumType.Types.CollisionPrefixEnum.Unspecified)] + [InlineData("FOO", CollisionPrefixEnumType.Types.CollisionPrefixEnum.Foo)] + [InlineData("COLLISION_PREFIX_ENUM_FOO", CollisionPrefixEnumType.Types.CollisionPrefixEnum.CollisionPrefixEnumFoo)] // Match exact rather than fallback. + [InlineData("COLLISION_PREFIX_ENUM_COLLISION_PREFIX_ENUM_FOO", CollisionPrefixEnumType.Types.CollisionPrefixEnum.CollisionPrefixEnumFoo)] + public void Enum_RemovePrefix_Collision_ReadString(string value, CollisionPrefixEnumType.Types.CollisionPrefixEnum expectedValue) + { + var serviceDescriptorRegistry = new DescriptorRegistry(); + serviceDescriptorRegistry.RegisterFileDescriptor(JsonTranscodingGreeter.Descriptor.File); + + var json = @$"{{ ""singleEnum"": ""{value}"" }}"; + + var result = AssertReadJson(json, descriptorRegistry: serviceDescriptorRegistry, serializeOld: false, settings: new GrpcJsonSettings { RemoveEnumPrefix = true }); + Assert.Equal(expectedValue, result.SingleEnum); } [Fact] diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterWriteTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterWriteTests.cs index 7dfe0c38095e..a735afa4e458 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterWriteTests.cs +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ConverterTests/JsonConverterWriteTests.cs @@ -489,33 +489,83 @@ public void FieldMask_Root() } [Theory] - [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Unspecified)] - [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Bar)] - [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Neg)] - [InlineData((HelloRequest.Types.DataTypes.Types.NestedEnum)100)] - public void Enum(HelloRequest.Types.DataTypes.Types.NestedEnum value) + [InlineData(PrefixEnumType.Types.PrefixEnum.Unspecified, @"""UNSPECIFIED""")] + [InlineData(PrefixEnumType.Types.PrefixEnum.Foo, @"""FOO""")] + [InlineData(PrefixEnumType.Types.PrefixEnum.Bar, @"""BAR""")] + [InlineData(PrefixEnumType.Types.PrefixEnum.Baz, @"""BAZ""")] + [InlineData(PrefixEnumType.Types.PrefixEnum._123, @"""_123""")] + [InlineData((PrefixEnumType.Types.PrefixEnum)100, "100")] + public void Enum_RemovePrefix(PrefixEnumType.Types.PrefixEnum value, string expectedString) + { + var dataTypes = new PrefixEnumType + { + SingleEnum = value + }; + + var json = AssertWrittenJson(dataTypes, settings: new GrpcJsonSettings { RemoveEnumPrefix = true }, compareOldNew: false); + Assert.Equal(@$"{{""singleEnum"":{expectedString}}}", json); + } + + [Theory] + [InlineData(PrefixEnumType.Types.PrefixEnum.Unspecified, @"""PREFIX_ENUM_UNSPECIFIED""")] + [InlineData(PrefixEnumType.Types.PrefixEnum.Foo, @"""PREFIX_ENUM_FOO""")] + [InlineData(PrefixEnumType.Types.PrefixEnum.Bar, @"""BAR""")] + [InlineData(PrefixEnumType.Types.PrefixEnum._123, @"""PREFIX_ENUM_123""")] + public void Enum_NoRemovePrefix(PrefixEnumType.Types.PrefixEnum value, string expectedString) + { + var dataTypes = new PrefixEnumType + { + SingleEnum = value + }; + + var json = AssertWrittenJson(dataTypes, compareOldNew: false); + Assert.Equal(@$"{{""singleEnum"":{expectedString}}}", json); + } + + [Theory] + [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Unspecified, null)] + [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Bar, @"""BAR""")] + [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Neg, @"""NEG""")] + [InlineData((HelloRequest.Types.DataTypes.Types.NestedEnum)100, "100")] + public void Enum(HelloRequest.Types.DataTypes.Types.NestedEnum value, string? expectedString) { var dataTypes = new HelloRequest.Types.DataTypes { SingleEnum = value }; - AssertWrittenJson(dataTypes); + var json = AssertWrittenJson(dataTypes, settings: new GrpcJsonSettings { IgnoreDefaultValues = true }); + if (expectedString != null) + { + Assert.Equal(@$"{{""singleEnum"":{expectedString}}}", json); + } + else + { + Assert.Equal("{}", json); + } } [Theory] - [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Unspecified)] - [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Bar)] - [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Neg)] - [InlineData((HelloRequest.Types.DataTypes.Types.NestedEnum)100)] - public void Enum_WriteNumber(HelloRequest.Types.DataTypes.Types.NestedEnum value) + [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Unspecified, null)] + [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Bar, "2")] + [InlineData(HelloRequest.Types.DataTypes.Types.NestedEnum.Neg, "-1")] + [InlineData((HelloRequest.Types.DataTypes.Types.NestedEnum)100, "100")] + public void Enum_WriteNumber(HelloRequest.Types.DataTypes.Types.NestedEnum value, string? expectedString) { var dataTypes = new HelloRequest.Types.DataTypes { SingleEnum = value }; - AssertWrittenJson(dataTypes, new GrpcJsonSettings { WriteEnumsAsIntegers = true, IgnoreDefaultValues = true }); + var json = AssertWrittenJson(dataTypes, new GrpcJsonSettings { WriteEnumsAsIntegers = true, IgnoreDefaultValues = true }); + if (expectedString != null) + { + Assert.Equal(@$"{{""singleEnum"":{expectedString}}}", json); + } + else + { + Assert.Equal("{}", json); + } } [Fact] @@ -537,7 +587,7 @@ public void JsonNamePriority() Assert.Equal(@"{""b"":10,""a"":20,""d"":30}", json); } - private string AssertWrittenJson(TValue value, GrpcJsonSettings? settings = null, bool? compareRawStrings = null) where TValue : IMessage + private string AssertWrittenJson(TValue value, GrpcJsonSettings? settings = null, bool? compareRawStrings = null, bool? compareOldNew = null) where TValue : IMessage { var typeRegistery = TypeRegistry.FromFiles( HelloRequest.Descriptor.File, @@ -563,11 +613,14 @@ private string AssertWrittenJson(TValue value, GrpcJsonSettings? setting _output.WriteLine("New:"); _output.WriteLine(jsonNew); - using var doc1 = JsonDocument.Parse(jsonNew); - using var doc2 = JsonDocument.Parse(jsonOld); + if (compareOldNew ?? true) + { + using var doc1 = JsonDocument.Parse(jsonNew); + using var doc2 = JsonDocument.Parse(jsonOld); - var comparer = new JsonElementComparer(maxHashDepth: -1, compareRawStrings: compareRawStrings ?? false); - Assert.True(comparer.Equals(doc1.RootElement, doc2.RootElement)); + var comparer = new JsonElementComparer(maxHashDepth: -1, compareRawStrings: compareRawStrings ?? false); + Assert.True(comparer.Equals(doc1.RootElement, doc2.RootElement)); + } return jsonNew; } diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto index 152e434607da..3c61333ad821 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto @@ -240,3 +240,23 @@ message FieldNameCaseMessage { int32 a = 1; int32 b = 2 [json_name="A"]; } + +message PrefixEnumType { + enum PrefixEnum { + PREFIX_ENUM_UNSPECIFIED = 0; + PREFIX_ENUM_FOO = 1; + BAR = 2; + BAZ = 3; + PREFIX_ENUM_123 = 4; + } + PrefixEnum single_enum = 1; +} + +message CollisionPrefixEnumType { + enum CollisionPrefixEnum { + COLLISION_PREFIX_ENUM_UNSPECIFIED = 0; + COLLISION_PREFIX_ENUM_FOO = 1; + COLLISION_PREFIX_ENUM_COLLISION_PREFIX_ENUM_FOO = 2; + } + CollisionPrefixEnum single_enum = 1; +}