Search code examples
c#asp.net-coreswaggerswagger-codegennswagstudio

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

Solution

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

    enter image description here