.net10 enum可视化显示到scalar中

由于.net 10 openapi 3.1的破坏性更新导致.net 9之前用的OpenApiArray等类型都不再可用,转而是使用JsonArray来替代,并使用JsonNodeExtension进行包装。

using System.Text.Json.Nodes;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

namespace MyNameSpace;

public class EnumSchemaTransformer : IOpenApiSchemaTransformer
{
    public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
    {
        var type = context.JsonTypeInfo.Type;
        var enumType = Nullable.GetUnderlyingType(type) ?? type;
        if (!enumType.IsEnum) return Task.CompletedTask;

        schema.Extensions ??= new Dictionary<string, IOpenApiExtension>();

        // Add x-enum-varnames
        var names = Enum.GetNames(enumType);
        var namesArray = new JsonArray();
        foreach (var name in names)
        {
            namesArray.Add(JsonValue.Create(name));
        }
        schema.Extensions["x-enum-varnames"] = new JsonNodeExtension(namesArray);

        // Add x-enum-descriptions
        var descriptions = EnumDescriptionProvider.GetDescriptions(enumType);
        if (descriptions is not { Length: > 0 }) return Task.CompletedTask;
        var descArray = new JsonArray();
        foreach (var desc in descriptions)
        {
            descArray.Add(JsonValue.Create(desc));
        }
        schema.Extensions["x-enum-descriptions"] = new JsonNodeExtension(descArray);

        // Add enum
        var valueArray = new JsonArray();
        foreach (var value in Enum.GetValues(enumType))
        {
            valueArray.Add(JsonValue.Create((int)value!));
        }
        schema.Extensions["enum"] = new JsonNodeExtension(valueArray);
        return Task.CompletedTask;
    }
}

其中的EnumDescriptionProvider.GetDescriptions(enumType)是使用源生成器生成的代码。以下为生成器的代码,支持从当前项目中读取枚举类型的xml注释或Description特性内容到一个字典中。源生成器的使用方式请见下一篇文章。

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace MyNameSpace;

[Generator]
public class EnumDescriptionGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
#if DEBUG
        //System.Diagnostics.Debugger.Launch();
#endif
        var enumDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (s, _) => s is EnumDeclarationSyntax,
                transform: static (ctx, _) => GetEnumInfo(ctx))
            .Where(static m => m is not null);

        var compilationAndEnums = context.CompilationProvider.Combine(enumDeclarations.Collect());

        context.RegisterSourceOutput(compilationAndEnums,
            static (spc, source) => Execute(source.Left, source.Right!, spc));
    }

    private static EnumInfo? GetEnumInfo(GeneratorSyntaxContext context)
    {
        var enumDeclaration = (EnumDeclarationSyntax)context.Node;
        if (context.SemanticModel.GetDeclaredSymbol(enumDeclaration) is not INamedTypeSymbol enumSymbol)
            return null;

        var members = new List<EnumMemberInfo>();
        foreach (var member in enumSymbol.GetMembers().OfType<IFieldSymbol>())
        {
            if (member.ConstantValue == null) continue;

            var description = member.GetAttributes()
                .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "System.ComponentModel.DescriptionAttribute")
                ?.ConstructorArguments.FirstOrDefault().Value?.ToString();

            if (description == null)
            {
                if (member.GetDocumentationCommentXml() is { Length: > 0 } xml)
                {
                    var summaryStart = xml.IndexOf("<summary>");
                    if (summaryStart != -1)
                    {
                        var summaryEnd = xml.IndexOf("</summary>", summaryStart);
                        if (summaryEnd != -1)
                        {
                            description = xml.Substring(summaryStart + 9, summaryEnd - summaryStart - 9).Trim();
                        }
                    }
                }
            }

            members.Add(new EnumMemberInfo(member.Name, description ?? member.Name));
        }

        return new EnumInfo(enumSymbol.ToDisplayString(), members);
    }

    private static void Execute(Compilation compilation, ImmutableArray<EnumInfo?> enums, SourceProductionContext context)
    {
        var validEnums = enums.Where(e => e != null).Select(e => e!).ToList();
        if (validEnums.Count == 0) return;

        var sb = new StringBuilder();
        sb.AppendLine("// <auto-generated />");
        sb.AppendLine("using System;");
        sb.AppendLine("using System.Collections.Generic;");
        sb.AppendLine("");
        sb.AppendLine("namespace MyNameSpace;");
        sb.AppendLine("");
        sb.AppendLine("public static class EnumDescriptionProvider");
        sb.AppendLine("{");
        sb.AppendLine("    private static readonly Dictionary<Type, string[]> _descriptions = new Dictionary<Type, string[]>()");
        sb.AppendLine("    {");

        var distinctEnums = new Dictionary<string, EnumInfo>();
        foreach (var info in validEnums)
        {
            if (!distinctEnums.ContainsKey(info.FullName))
            {
                distinctEnums[info.FullName] = info;
            }
        }

        foreach (var info in distinctEnums.Values)
        {
            var descriptions = string.Join(", ", info.Members.Select(m => $"\"{m.Description.Replace("\"", "\\\"")}\""));
            sb.AppendLine($"        {{ typeof({info.FullName}), new string[] {{ {descriptions} }} }},");
        }

        sb.AppendLine("    };");
        sb.AppendLine("");
        sb.AppendLine("    public static string[] GetDescriptions(Type type)");
        sb.AppendLine("    {");
        sb.AppendLine("        return _descriptions.TryGetValue(type, out var descriptions) ? descriptions : Array.Empty<string>();");
        sb.AppendLine("    }");
        sb.AppendLine("}");

        context.AddSource("EnumDescriptionProvider.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
    }

    private class EnumInfo
    {
        public string FullName { get; }
        public List<EnumMemberInfo> Members { get; }

        public EnumInfo(string fullName, List<EnumMemberInfo> members)
        {
            FullName = fullName;
            Members = members;
        }
    }

    private class EnumMemberInfo
    {
        public string Name { get; }
        public string Description { get; }

        public EnumMemberInfo(string name, string description)
        {
            Name = name;
            Description = description;
        }
    }
}

生成出的代码大概如下:

// <auto-generated />
using System;
using System.Collections.Generic;

namespace VTrust.VideoMeeting.Web.Server;

public static class EnumDescriptionProvider
{
    private static readonly Dictionary<Type, string[]> _descriptions = new Dictionary<Type, string[]>()
    {
        { typeof(MyNameSpace.Dtos.JoinState), new string[] { "待加入", "已加入", "已离开" } },
    };

    public static string[] GetDescriptions(Type type)
    {
        return _descriptions.TryGetValue(type, out var descriptions) ? descriptions : Array.Empty<string>();
    }
}

最后是EnumSchemaTransformer的使用方式。修改Program.cs中关于AddOpenApi()的部分为以下代码

builder.Services.AddOpenApi(options =>
        {
            options.AddDocumentTransformer((document, _, _) =>
            {
                document.Info.Title = "xxxx API";
                document.Info.Version = "v1";
                document.Info.Description = "XXXXXXXXXXX";
                return Task.CompletedTask;
            });
            options.AddSchemaTransformer<EnumSchemaTransformer>();
        });

最后在scalar中显示的效果如下
image

posted @ 2025-12-22 14:51  turingguo  阅读(24)  评论(0)    收藏  举报