You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过源生成自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.06 01:14:51