如何通过源生成自定义System.Text.Json.JsonStringEnumConverter<TEnum>?有无更优方案?
.NET 6下System.Text.Json源生成字符串枚举转换器实现指南
1. 是否在重复造轮子?
没有重复造轮子。.NET 7及以上版本的System.Text.Json才引入泛型版本的JsonStringEnumConverter<TEnum>,而.NET 6官方并未提供该泛型转换器;同时你需要适配AOT场景,必须用源生成替代反射实现的JsonStringEnumConverter,所以你的方案合理且必要。
2. 源生成器具体实现步骤
步骤1:定义标记用的部分类
在业务项目中创建以下空部分类,作为源生成器的识别标记:
using System.Text.Json; public partial class SourceGenStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum { // 空实现,源生成器将自动补充Read/Write方法 }
添加泛型约束where TEnum : struct, Enum,确保类型安全。
步骤2:创建源生成器项目
新建一个.NET Standard 2.0类库项目,添加以下NuGet包引用:
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
步骤3:编写源生成器核心逻辑
实现IIncrementalGenerator接口,核心逻辑是识别带有目标转换器特性的枚举,生成硬编码的互转实现:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Text; [Generator] public class StringEnumConverterGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { // 筛选所有标记了SourceGenStringEnumConverter的枚举 var enumDeclarations = context.SyntaxProvider .CreateSyntaxProvider( predicate: (s, _) => s is EnumDeclarationSyntax, transform: (ctx, _) => GetTargetEnumInfo(ctx)) .Where(info => info != null); // 生成对应转换器代码 context.RegisterSourceOutput(enumDeclarations, (spc, info) => GenerateConverterCode(spc, info!)); } private EnumInfo? GetTargetEnumInfo(GeneratorSyntaxContext context) { var enumDecl = (EnumDeclarationSyntax)context.Node; var enumSymbol = context.SemanticModel.GetDeclaredSymbol(enumDecl) as INamedTypeSymbol; if (enumSymbol == null) return null; // 查找JsonConverter特性 var converterAttr = enumSymbol.GetAttributes() .FirstOrDefault(attr => attr.AttributeClass?.Name == "JsonConverterAttribute"); if (converterAttr == null) return null; // 验证特性参数是否为SourceGenStringEnumConverter<TEnum> var arg = converterAttr.ConstructorArguments.FirstOrDefault(); if (arg.Value is not INamedTypeSymbol converterType) return null; if (converterType.ConstructedFrom.Name != "SourceGenStringEnumConverter") return null; // 确认泛型参数为当前枚举类型 var enumTypeArg = converterType.TypeArguments.FirstOrDefault(); if (enumTypeArg != enumSymbol) return null; // 收集枚举成员及对应字符串(支持EnumMember自定义名称) var members = enumSymbol.GetMembers() .OfType<IFieldSymbol>() .Where(f => f.IsStatic && f.IsConst) .Select(f => new EnumMemberInfo( Name: f.Name, Value: (int)f.ConstantValue!, StringValue: GetCustomEnumMemberName(f))) .ToList(); return new EnumInfo( FullTypeName: enumSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ShortTypeName: enumSymbol.Name, Members: members); } private string GetCustomEnumMemberName(IFieldSymbol field) { var enumMemberAttr = field.GetAttributes() .FirstOrDefault(attr => attr.AttributeClass?.Name == "EnumMemberAttribute"); if (enumMemberAttr != null && enumMemberAttr.ConstructorArguments.FirstOrDefault().Value is string customName) { return customName; } return field.Name; } private void GenerateConverterCode(SourceProductionContext context, EnumInfo info) { var codeBuilder = new StringBuilder(); codeBuilder.Append($@" using System; using System.Text.Json; using System.Text.Json.Serialization; public partial class SourceGenStringEnumConverter<{info.ShortTypeName}> : JsonConverter<{info.ShortTypeName}> {{ public override {info.ShortTypeName} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {{ if (reader.TokenType != JsonTokenType.String) {{ throw new JsonException(""Expected string token for enum {info.ShortTypeName}""); }} var value = reader.GetString(); return value switch {{"); // 添加Read方法的switch分支 foreach (var member in info.Members) { codeBuilder.Append($@" ""{member.StringValue}"" => {info.FullTypeName}.{member.Name},"); } codeBuilder.Append($@" _ => throw new JsonException($""Unknown {info.ShortTypeName} value: {{value}}"") }}; }} public override void Write(Utf8JsonWriter writer, {info.ShortTypeName} value, JsonSerializerOptions options) {{ var stringValue = value switch {{"); // 添加Write方法的switch分支 foreach (var member in info.Members) { codeBuilder.Append($@" {info.FullTypeName}.{member.Name} => ""{member.StringValue}"","); } codeBuilder.Append($@" _ => throw new JsonException($""Unknown {info.ShortTypeName} value: {{value}}"") }}; writer.WriteStringValue(stringValue); }} }}"); // 生成源文件,避免重复 context.AddSource($"SourceGenStringEnumConverter_{info.ShortTypeName}.g.cs", codeBuilder.ToString()); } // 辅助数据结构 private record EnumInfo(string FullTypeName, string ShortTypeName, List<EnumMemberInfo> Members); private record EnumMemberInfo(string Name, int Value, string StringValue); }}
步骤4:引用源生成器
在业务项目的csproj中添加对源生成器项目的引用,配置为分析器:
<ProjectReference Include="..\YourGeneratorProject\YourGeneratorProject.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
步骤5:使用枚举
保持你原本的枚举定义即可:
using System.Text.Json.Serialization; [JsonConverter(typeof(SourceGenStringEnumConverter<DayOfWeek>))] public enum DayOfWeek { Monday, Tuesday, EtcDay };
可选扩展
- 如果不需要
EnumMemberAttribute支持,可以删除GetCustomEnumMemberName方法及相关逻辑 - 错误处理可根据需求调整,比如返回枚举默认值而非抛出异常
- 若要支持Flags枚举,可扩展Write方法,处理多值组合的字符串拼接逻辑
内容的提问来源于stack exchange,提问作者Jeffery Chow
相关产品推荐
相关产品推荐

