【问题标题】:Swagger C# Enum generation - underlying int values do not match the original enumSwagger C# 枚举生成 - 基础 int 值与原始枚举不匹配
【发布时间】:2020-05-30 00:36:40
【问题描述】:

我在我的服务器上创建了一个手动设置整数值的枚举,而不是从 0 开始的默认增量

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

我的服务器使用 AspNetCore.App 2.2.0 运行。在 Startup.cs 中使用 swashbuckle aspnetcore 4.0.1 进行配置,以在每次启动服务器时生成一个 swagger json 文件来描述该 api。

然后,我使用适用于 windows v 13.2.3.0 的 NSwag Studio 生成带有该 swagger JSON 文件的 C sharp api 客户端,用于 Xamarin 应用程序。在生成的 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,

}

这给我的客户端带来了问题,因为在某些情况下我需要知道整数值。我正在寻找一种解决方案,每次我想知道客户端的整数值时,我都可以避免编写转换器。

选项 1: 在 NSwag Studio 或 .net 配置(我的 Startup.Cs 配置在下面供参考)中是否有一个选项我可以强制生成的枚举获得与原始枚举相同的整数值?

选项 2: 或者,如果没有,我的客户端和我的服务器都可以通过共享类库访问相同的原始枚举。有没有办法让生成的 api 客户端使用 apiclient.cs 中的实际原始枚举而不是自己生成?

参考:

我在 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...
                  }

【问题讨论】:

  • 如果您查看 Swashbuckle 生成的 swagger.json,您会发现,一旦您选择将枚举作为文本值发出,整数值对客户端完全不可用(我认为它只是坚持 OAS 规范)。因此 NSwag 无法知道文本后面的整数值。您可以改用整数,但文本值具有您可能不想失去的优势,在这种情况下,您必须考虑为什么客户端需要两者,因为枚举的语义可能会以这种方式有点延伸。

标签: c# asp.net-core swagger swagger-codegen nswagstudio


【解决方案1】:

所以这是我正在使用的两个枚举助手。一个由 NSwag (x-enumNames) 使用,另一个由 Azure AutoRest (x-ms-enums) 使用

终于找到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);
            }
        }
    }
}

然后在你的 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>();
        });

这应该在 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"
              }
            ]
          }
        },

这个解决方案有一个问题,(我没有时间研究) 枚举名称是用我在 NSwag 中的 DTO 名称生成的吗?如果您确实找到了解决方案,请告诉我:-)

例如,以下 Enum 是使用 NSwag 生成的:

【讨论】:

  • 哇,Dawood 非常感谢 - 我将您的代码复制到我的解决方案中,编译它,它第一次工作!我真的很感动。至于名称 - 你可以做的一件事是使用 setup.UseReferencedDefinitionsForEnums();在 AddSwaggerGen 方法中。这将生成在 api 调用之间共享的枚举,并完全按照您的原始枚举命名。在客户端,如果它可以同时看到原始枚举和 api 版本,这可能会导致头痛,但如果你重构以便客户端实体直接引用 api 生成的枚举,你应该没问题。再次感谢!
  • @AdamDiament 您的枚举是否在 swagger.json 中生成与服务器相同?我不明白重构部分 - 因为我的枚举用于 DTO?
  • 枚举在 swagger json 中生成为新枚举,但名称与服务器上的枚举完全相同。我已经重构了我的 dtos / viewmodels 客户端以引用新的 api 枚举而不是服务器枚举,并删除客户端上对定义服务器枚举的命名空间的所有引用。然后,当 api 客户端使用自己的枚举发送 http 调用时,它们会被反序列化为服务器上的服务器枚举,而不会出现问题。
【解决方案2】:

* 更新 *

dawood 在下面的答案中发布了一个可行的解决方案,这正是我想要的。

* 原始答案 * 目前似乎没有办法做到这一点。正如@sellotape 在他的评论中提到的那样,这甚至可能不是一个好主意。由于我控制着服务器并且它是一个相对较新的项目,我已将我的枚举重构为正常的“从零开始的顺序”样式。

我确实认为它对某些用例很有用 - 例如。支持不能轻易重构的遗留枚举,或者能够对中间有间隙的枚举进行编号,例如10,20,30。这将允许稍后插入 11,12 等,同时保留将某种“顺序”编码到您的枚举的能力,并且不会随着项目的增长而破坏该顺序。

不过,目前看来不可能,所以我们将继续这样做。

【讨论】:

  • 所以你改用枚举的整数值了吗?
  • 否 @DawoodAwan 我保留了字符串值,因为它们比仅使用整数更有用,但我将原始枚举重构为连续整数 0、1、2、3 等,以确保它们将匹配我需要知道客户端的 int 值的用例
  • 如果你使用 int 值,你可以创建一个 swagger helper 来生成 x-enums 值——在使用 nSwag 时它会生成整数和字符串值——但是因为你在哪里使用字符串值我认为这是一个无效的答案。
  • @DawoodAwan 听起来很有希望 - 你能发布一个代码示例吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-21
  • 2016-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多