简体   繁体   English

Swagger C# 枚举生成 - 基础 int 值与原始枚举不匹配

[英]Swagger C# Enum generation - underlying int values do not match the original enum

I created an enum on my server with integer values set manually rather than the default increment up from 0我在我的服务器上创建了一个手动设置整数值的枚举,而不是从 0 开始的默认增量

public enum UserType
{
    Anonymous = 0,
    Customer = 10,
    Technician = 21,
    Manager = 25,
    Primary = 30
}

My server is running using AspNetCore.App 2.2.0.我的服务器使用 AspNetCore.App 2.2.0 运行。 It's configured in Startup.cs with swashbuckle aspnetcore 4.0.1 to generate a swagger json file to describe the api every time the server is started.它在 Startup.cs 中使用 swashbuckle aspnetcore 4.0.1 进行配置,以在每次启动服务器时生成一个 swagger json 文件来描述 api。

I then use NSwag Studio for windows v 13.2.3.0 to generate a C sharp api client with that swagger JSON file, for use in a Xamarin app.然后,我使用 NSwag Studio for windows v 13.2.3.0 生成带有该 swagger JSON 文件的 C sharp api 客户端,用于 Xamarin 应用程序。 The generated enum in the resulting c sharp api client looks like this - the underlying integer values do not match the original enum.生成的 c sharp api 客户端中生成的枚举如下所示 - 基础整数值与原始枚举不匹配。

[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.5.0 (Newtonsoft.Json v11.0.0.0)")]
public enum UserType
{
    [System.Runtime.Serialization.EnumMember(Value = @"Anonymous")]
    Anonymous = 0,

    [System.Runtime.Serialization.EnumMember(Value = @"Customer")]
    Customer = 1,

    [System.Runtime.Serialization.EnumMember(Value = @"Technician")]
    Technician = 2,

    [System.Runtime.Serialization.EnumMember(Value = @"Manager")]
    Manager = 3,

    [System.Runtime.Serialization.EnumMember(Value = @"Primary")]
    Primary = 4,

}

This creates a problem for me client side as there are situations where I need to know the integer value.这给我的客户端带来了一个问题,因为在某些情况下我需要知道整数值。 I am looking for a solution where I can avoid writing converters every time I want to know the integer value on the client side.我正在寻找一种解决方案,每次我想知道客户端的整数值时,我都可以避免编写转换器。

Option 1: Is there an option I am missing in either NSwag Studio or in .net configuration (my Startup.Cs config is below for reference) where I can force the generated enums to get the same integer values as the original enum?选项 1:我在 NSwag Studio 或 .net 配置中是否缺少一个选项(我的 Startup.Cs 配置在下面供参考),我可以强制生成的枚举获得与原始枚举相同的整数值?

Option 2: Alternatively if not, both my client and my server have access to the same original enum via a shared class library.选项 2:或者,如果没有,我的客户端和我的服务器都可以通过共享类库访问相同的原始枚举。 Is there a way to get the generated api client to use the actual original enums in the apiclient.cs rather than generate its own?有没有办法让生成的 api 客户端使用 apiclient.cs 中的实际原始枚举而不是生成自己的?

Reference:参考:

The enums part of my swagger generation code in Startup.Cs looks like this我在 Startup.Cs 中的招摇生成代码的枚举部分看起来像这样

services.AddJsonOptions(options =>
{
   options.SerializerSettings.Converters.Add(new StringEnumConverter());
....

services.AddSwaggerGen(setup =>
{
   setup.SwaggerDoc("v1", new Info { Title = AppConst.SwaggerTitle, Version = "v1" });

   setup.UseReferencedDefinitionsForEnums();
   ... other stuff...
 }

@Dawood answer is a masterpiece @Dawood 答案是杰作
But it works only on old versions of Swashbuckle (I am not sure which versions)但它仅适用于旧版本的Swashbuckle (我不确定哪些版本)
If you have Swashbuckle 6.x that code will NOT compile.如果您有Swashbuckle 6.x ,则该代码将无法编译。
Here is the same solution but works for Swashbuckle 6.x这是相同的解决方案,但适用于Swashbuckle 6.x

using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

/// <summary>
/// Add enum value descriptions to Swagger
/// https://stackoverflow.com/a/49941775/1910735
/// </summary>
public class EnumDocumentFilter : IDocumentFilter
{
    /// <inheritdoc />
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        foreach (KeyValuePair<string, OpenApiPathItem> schemaDictionaryItem in swaggerDoc.Paths)
        {
            OpenApiPathItem schema = schemaDictionaryItem.Value;
            foreach (OpenApiParameter property in schema.Parameters)
            {
                IList<IOpenApiAny> propertyEnums = property.Schema.Enum;
                if (propertyEnums.Count > 0)
                    property.Description += DescribeEnum(propertyEnums);
            }
        }

        if (swaggerDoc.Paths.Count == 0)
            return;

        // add enum descriptions to input parameters
        foreach (OpenApiPathItem pathItem in swaggerDoc.Paths.Values)
        {
            DescribeEnumParameters(pathItem.Parameters);

            foreach (KeyValuePair<OperationType, OpenApiOperation> operation in pathItem.Operations)
                DescribeEnumParameters(operation.Value.Parameters);
        }
    }

    private static void DescribeEnumParameters(IList<OpenApiParameter> parameters)
    {
        if (parameters == null)
            return;

        foreach (OpenApiParameter param in parameters)
        {
            if (param.Schema.Enum?.Any() == true)
            {
                param.Description += DescribeEnum(param.Schema.Enum);
            }
            else if (param.Extensions.ContainsKey("enum") && 
                     param.Extensions["enum"] is IList<object> paramEnums &&
                     paramEnums.Count > 0)
            {
                param.Description += DescribeEnum(paramEnums);
            }
        }
    }

    private static string DescribeEnum(IEnumerable<object> enums)
    {
        List<string> enumDescriptions = new();
        Type? type = null;
        foreach (object enumOption in enums)
        {
            if (type == null)
                type = enumOption.GetType();

            enumDescriptions.Add($"{Convert.ChangeType(enumOption, type.GetEnumUnderlyingType())} = {Enum.GetName(type, enumOption)}");
        }

        return Environment.NewLine + string.Join(Environment.NewLine, enumDescriptions);
    }
}
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

//https://stackoverflow.com/a/60276722/4390133
public class EnumFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (schema is null)
            throw new ArgumentNullException(nameof(schema));

        if (context is null)
            throw new ArgumentNullException(nameof(context));

        if (context.Type.IsEnum is false)
            return;

        schema.Extensions.Add("x-ms-enum", new EnumFilterOpenApiExtension(context));
    }
}
using System.Text.Json;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Writers;
using Swashbuckle.AspNetCore.SwaggerGen;

public class EnumFilterOpenApiExtension : IOpenApiExtension
{
    private readonly SchemaFilterContext _context;
    public EnumFilterOpenApiExtension(SchemaFilterContext context)
    {
        _context = context;
    }

    public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion)
    {
        JsonSerializerOptions options = new() { WriteIndented = true };

        var obj = new {
            name = _context.Type.Name,
            modelAsString = false,
            values = _context.Type
                            .GetEnumValues()
                            .Cast<object>()
                            .Distinct()
                            .Select(value => new { value, name = value.ToString() })
                            .ToArray()
        };
        writer.WriteRaw(JsonSerializer.Serialize(obj, options));
    }
}
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

/// <summary>
/// Adds extra schema details for an enum in the swagger.json i.e. x-enumNames (used by NSwag to generate Enums for C# client)
/// https://github.com/RicoSuter/NSwag/issues/1234
/// </summary>
public class NSwagEnumExtensionSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (schema is null)
            throw new ArgumentNullException(nameof(schema));

        if (context is null)
            throw new ArgumentNullException(nameof(context));

        if (context.Type.IsEnum)
            schema.Extensions.Add("x-enumNames", new NSwagEnumOpenApiExtension(context));
    }
}
using System.Text.Json;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Writers;
using Swashbuckle.AspNetCore.SwaggerGen;

public class NSwagEnumOpenApiExtension : IOpenApiExtension
{
    private readonly SchemaFilterContext _context;
    public NSwagEnumOpenApiExtension(SchemaFilterContext context)
    {
        _context = context;
    }

    public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion)
    {
        string[] enums = Enum.GetNames(_context.Type);
        JsonSerializerOptions options = new() { WriteIndented = true };
        string value = JsonSerializer.Serialize(enums, options);
        writer.WriteRaw(value);
    }
}

and Last thing, The registerations of the Filters最后一件事,过滤器的注册

services.AddSwaggerGen(c =>
{
    ... the rest of your configuration

    // REMOVE THIS to use Integers for Enums
    // c.DescribeAllEnumsAsStrings();

    // add enum generators based on whichever code generators you decide
    c.SchemaFilter<NSwagEnumExtensionSchemaFilter>();
    c.SchemaFilter<EnumFilter>();
});

NOTES笔记

  1. I am using C# 10 features (Implicit usings and others) IF YOU DO NOT USE C# 10, THEN YOU HAVE TO ADD SOME USING STATEMENTS AND REVERT TO THE OLD NAMESPACE STYLE AND DO SOME OTHER SMALL MODIFICATIONS FOR THE SAKE OF C#我正在使用 C# 10 功能(隐式使用和其他功能)如果您不使用 C# 10,那么您必须添加一些使用语句并恢复到旧的命名空间样式并为 C# 进行一些其他小的修改
  2. I tested this code and the results are the same as the original answer我测试了这段代码,结果和原来的答案一样

So these are the two Enum Helpers I'm using.所以这些是我正在使用的两个枚举助手。 One is used by NSwag ( x-enumNames ) and the other is used by Azure AutoRest ( x-ms-enums )一个由 NSwag ( x-enumNames ) 使用,另一个由 Azure AutoRest ( x-ms-enums ) 使用

Finally found the reference for EnumDocumentFilter ( https://stackoverflow.com/a/49941775/1910735 )终于找到了EnumDocumentFilter的参考( https://stackoverflow.com/a/49941775/1910735

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace SwaggerDocsHelpers
{
    /// <summary>
    /// Add enum value descriptions to Swagger
    /// https://stackoverflow.com/a/49941775/1910735
    /// </summary>
    public class EnumDocumentFilter : IDocumentFilter
    {
        /// <inheritdoc />
        public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
        {
            // add enum descriptions to result models
            foreach (var schemaDictionaryItem in swaggerDoc.Definitions)
            {
                var schema = schemaDictionaryItem.Value;
                foreach (var propertyDictionaryItem in schema.Properties)
                {
                    var property = propertyDictionaryItem.Value;
                    var propertyEnums = property.Enum;
                    if (propertyEnums != null && propertyEnums.Count > 0)
                    {
                        property.Description += DescribeEnum(propertyEnums);
                    }
                }
            }

            if (swaggerDoc.Paths.Count <= 0) return;

            // add enum descriptions to input parameters
            foreach (var pathItem in swaggerDoc.Paths.Values)
            {
                DescribeEnumParameters(pathItem.Parameters);

                // head, patch, options, delete left out
                var possibleParameterisedOperations = new List<Operation> { pathItem.Get, pathItem.Post, pathItem.Put };
                possibleParameterisedOperations.FindAll(x => x != null)
                    .ForEach(x => DescribeEnumParameters(x.Parameters));
            }
        }

        private static void DescribeEnumParameters(IList<IParameter> parameters)
        {
            if (parameters == null) return;

            foreach (var param in parameters)
            {
                if (param is NonBodyParameter nbParam && nbParam.Enum?.Any() == true)
                {
                    param.Description += DescribeEnum(nbParam.Enum);
                }
                else if (param.Extensions.ContainsKey("enum") && param.Extensions["enum"] is IList<object> paramEnums &&
                  paramEnums.Count > 0)
                {
                    param.Description += DescribeEnum(paramEnums);
                }
            }
        }

        private static string DescribeEnum(IEnumerable<object> enums)
        {
            var enumDescriptions = new List<string>();
            Type type = null;
            foreach (var enumOption in enums)
            {
                if (type == null) type = enumOption.GetType();
                enumDescriptions.Add($"{Convert.ChangeType(enumOption, type.GetEnumUnderlyingType())} = {Enum.GetName(type, enumOption)}");
            }

            return $"{Environment.NewLine}{string.Join(Environment.NewLine, enumDescriptions)}";
        }
    }

    public class EnumFilter : ISchemaFilter
    {
        public void Apply(Schema model, SchemaFilterContext context)
        {
            if (model == null)
                throw new ArgumentNullException("model");

            if (context == null)
                throw new ArgumentNullException("context");


            if (context.SystemType.IsEnum)
            {

                var enumUnderlyingType = context.SystemType.GetEnumUnderlyingType();
                model.Extensions.Add("x-ms-enum", new
                {
                    name = context.SystemType.Name,
                    modelAsString = false,
                    values = context.SystemType
                    .GetEnumValues()
                    .Cast<object>()
                    .Distinct()
                    .Select(value =>
                    {
                        //var t = context.SystemType;
                        //var convereted = Convert.ChangeType(value, enumUnderlyingType);
                        //return new { value = convereted, name = value.ToString() };
                        return new { value = value, name = value.ToString() };
                    })
                    .ToArray()
                });
            }
        }
    }


    /// <summary>
    /// Adds extra schema details for an enum in the swagger.json i.e. x-enumNames (used by NSwag to generate Enums for C# client)
    /// https://github.com/RicoSuter/NSwag/issues/1234
    /// </summary>
    public class NSwagEnumExtensionSchemaFilter : ISchemaFilter
    {
        public void Apply(Schema model, SchemaFilterContext context)
        {
            if (model == null)
                throw new ArgumentNullException("model");

            if (context == null)
                throw new ArgumentNullException("context");


            if (context.SystemType.IsEnum)
            {
                var names = Enum.GetNames(context.SystemType);
                model.Extensions.Add("x-enumNames", names);
            }
        }
    }
}

Then in your startup.cs you configure them然后在你的 startup.cs 中配置它们

        services.AddSwaggerGen(c =>
        {
            ... the rest of your configuration

            // REMOVE THIS to use Integers for Enums
            // c.DescribeAllEnumsAsStrings();

            // add enum generators based on whichever code generators you decide
            c.SchemaFilter<NSwagEnumExtensionSchemaFilter>();
            c.SchemaFilter<EnumFilter>();
        });

This should generate your enums as this in the Swagger.json file这应该在 Swagger.json 文件中生成您的枚举

        sensorType: {
          format: "int32",
          enum: [
            0,
            1,
            2,
            3
          ],
          type: "integer",
          x-enumNames: [
            "NotSpecified",
            "Temperature",
            "Fuel",
            "Axle"
          ],
          x-ms-enum: {
            name: "SensorTypesEnum",
            modelAsString: false,
            values: [{
                value: 0,
                name: "NotSpecified"
              },
              {
                value: 1,
                name: "Temperature"
              },
              {
                value: 2,
                name: "Fuel"
              },
              {
                value: 3,
                name: "Axle"
              }
            ]
          }
        },

There is one issue with this solution though, (which I haven't had time to look into) Is that the Enum names are generated with my DTO names in NSwag - If you do find a solution to this do let me know :-)但是,此解决方案存在一个问题(我没有时间研究)是枚举名称是使用我在 NSwag 中的 DTO 名称生成的 - 如果您确实找到了解决方案,请告诉我 :-)

Example, the following Enum was generated using NSwag:例如,以下枚举是使用 NSwag 生成的:

在此处输入图像描述

UPDATE更新
dawood posted a working solution above that does exactly what I want it to. dawood在上面发布了一个工作解决方案,它完全符合我的要求。


ORIGINAL ANSWER原始答案
There appears to be no way to do this currently.目前似乎没有办法做到这一点。 As @sellotape mentioned in his comment, it may not even be a good idea.正如@sellotape 在他的评论中提到的,这甚至可能不是一个好主意。 Since I'm in control of the server and it's a relatively new project I have refactored my enum to be the normal "sequential from zero" style.由于我控制着服务器并且它是一个相对较新的项目,我已将我的枚举重构为正常的“从零开始的顺序”样式。

I do think it would be useful for some use-cases - eg supporting a legacy enum that can't be refactored easily, or the ability to number the enums with gaps in the middle eg 10,20,30.我确实认为它对某些用例很有用——例如,支持不能轻易重构的遗留枚举,或者能够对中间有间隙的枚举进行编号,例如 10、20、30。 This would enable inserting 11,12 etc at a later time, whilst retaining the ability to encode some kind of "order" to your enum and not break that order as the project grows.这将允许稍后插入 11,12 等,同时保留将某种“顺序”编码到您的枚举的能力,并且不会随着项目的增长而破坏该顺序。

At the moment it doesn't seem possible however, so we'll go with this.然而,目前这似乎不可能,所以我们将继续这样做。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM