简体   繁体   中英

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

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

My server is running using 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.

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. The generated enum in the resulting c sharp api client looks like this - the underlying integer values do not match the original enum.

[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?

Option 2: Alternatively if not, both my client and my server have access to the same original enum via a shared class library. 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?

Reference:

The enums part of my swagger generation code in Startup.Cs looks like this

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
But it works only on old versions of Swashbuckle (I am not sure which versions)
If you have Swashbuckle 6.x that code will NOT compile.
Here is the same solution but works for 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#
  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 )

Finally found the reference for 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

        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

        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 :-)

Example, the following Enum was generated using NSwag:

在此处输入图像描述

UPDATE
dawood posted a working solution above that does exactly what I want it to.


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. 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. 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.

At the moment it doesn't seem possible however, so we'll go with this.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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