Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving from NSwag to Swashbuckle #2748

Open
vvdb-architecture opened this issue Dec 7, 2023 · 5 comments
Open

Moving from NSwag to Swashbuckle #2748

vvdb-architecture opened this issue Dec 7, 2023 · 5 comments

Comments

@vvdb-architecture
Copy link

This is not so much of an issue, as a report of an attempt to move a nontrivial API with generics and inheritance from NSwag (13, 14-preview) to Swashbuckle 6.5.0 and describe the problems we had to solve to make the OpenAPI.json generation and the Swagger UI working "almost" as expected. Even after these problems were solved, issues remain.

Ambiguous types

The schema IDs of nested types and templated types are problematic with Swashbuckle (NSwag doesn't have this problem, but sometimes appends a number to the type name, which is confusing.)
After examining various solutions, we came up with this:

            c.CustomSchemaIds(SwashBuckleUtils.ToSchemaId);

The source for SwashBuckleUtils is shown at the end of this text. This seems to resolve most of our issues.

Inheritance and polymorphism

We first tried with:

            c.UseAllOfForInheritance();
            c.UseOneOfForPolymorphism();

The net effect is that the SwashBuckleUtils.ToSchemaId got called over 1500 times with almost all runtime types and generic types that our API ever used. When we tried:

            c.EnableAnnotations(enableAnnotationsForInheritance: true, enableAnnotationsForPolymorphism: true);

... it appeared to work correctly, even though setting both parameters to true ends up calling both c.UseXxxxForYyy methods.
It took a while before we figured out that in addition, the EnableAnnotations also calls:

                options.SelectSubTypesUsing(AnnotationsSubTypesSelector);
                options.SelectDiscriminatorNameUsing(AnnotationsDiscriminatorNameSelector);
                options.SelectDiscriminatorValueUsing(AnnotationsDiscriminatorValueSelector);

This limits the types greatly. But the delegate methods are private and cannot be called by user code.

Another problem is that NSwag automatially handles KnownTypeAttribute. In SwashBuckle, we needed to write:

        c.SelectSubTypesUsing(SwashBuckleUtils.AnnotationsKnownTypesSelector);

The source code can be found below.

Enum serialization

By default, ASP.NET Core serializes enums as their underlying scalar value (usually int). And yet, they are specified correctly in the generated OpenAPI.json document. Generated C# clients will reproduce the enums with their correct values.

Switch to Swashbuckle changing nothing else, the enums will be specified according to the way they are serialized. This means that a type like LogLevel will appear like this:

public enum LogLevel
    {
        _0 = 0,
        _1 = 1,
        _2 = 2,
        _3 = 3,
        _4 = 4,
        _5 = 5,
        _6 = 6,
    }

The only way to change that is to change the way enum is serialized in the API:

        services.AddControllers(opt =>
        {
            ...
        })
            .AddJsonOptions(options =>
            {
                // we need to add this to have the same behavior in SwashBuckle as NSwag
                options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            });

... but this means that all enums will now be strings, which are usually larger and therefore the data transfer will be larger as well!
No solution was found for this problem.

Closing thoughts

Issues remain. The generated OpenAPI inists on including System.TimeSpan and we don't understand why. There are other things with nullable types we cannot explain.

The ability to generate OpenAPI specifications from an ASP.NET Core API is one of the cornerstones of the current way we develop software and publish API's. It seems that we only have two options today: NSwag and Swashbuckle.

Both initiatives were started and are maintained by courageous and extremely smart people. It's complex software that needs to solve a complex problem. It seems strange to us that Microsoft doesn't realize this, and tries to help either (or both!) in achieving usage and feature parity with each other, and move both projects forward. They both have merit.

They did this with Json serialization (James Newton-King works for Microsoft now, and Microsoft has System.Text.Json). The did it with Polly (it's now integrated as part of their resilience features in .NET 8). They've integrated zlib (for ZIP/GZ (de)serialization.

It is a puzzle to us why they can't help out more for NSwag/Swashbuckle.

In any case, I hope this little experience overview and the source code below is of use to somebody.

Source code of SwashBuckleUtils:

static class SwashBuckleUtils
{
    private static readonly Dictionary<Type, string> _map = new Dictionary<Type, string>();

    private static string ToSchemaId(Type type, bool skipDeclaringType)
    {
        if (!_map.TryGetValue(type, out var name))
        {
            if (type.IsGenericType && !type.IsGenericTypeDefinition)
            {
                var genericTypeName = type.GetGenericTypeDefinition().Name;
                // Check if it's a real generic type. Non-generic types nested in generic types will also have IsGenericType() == true but won't have a '`' in their name
                // "Real" generic type names must be trimmed to exclude everything after '`' including that character.
                int p = genericTypeName.IndexOf('`');
                if (p > 0)
                    genericTypeName = genericTypeName[..p];
                name = $"{genericTypeName}Of{string.Join("And", type.GetGenericArguments().Select(t => ToSchemaId(t, skipDeclaringType: true)))}";
            }
            else
            {
                // the .Replace() handles the case of nested types, since '+' is not a valid schema id character
                name = type.Namespace + '.' + type.Name.Replace('+', '_');
                if (type.DeclaringType is not null && !skipDeclaringType)
                    name = ToSchemaId(type.DeclaringType) + '_' + name;
            }
            _map[type] = name;
        }
        return name;
    }

    public static string ToSchemaId(Type type)
    {
        var name = ToSchemaId(type, skipDeclaringType: false);
        return name;
    }

    public static IEnumerable<Type> AnnotationsKnownTypesSelector(Type type)
    {
        var subtypes = new List<Type>();

        foreach (var knownTypeAttribute in type.GetCustomAttributes(false).OfType<KnownTypeAttribute>())
        {
            if (knownTypeAttribute.Type is not null)
                subtypes.Add(knownTypeAttribute.Type);
            else if (knownTypeAttribute.MethodName is not null)
                throw new InvalidOperationException($"{type}: KnownType(\"{knownTypeAttribute.MethodName}\") is not supported");
        }

        return subtypes;
    }
}
@Berthelmaster
Copy link

Hi @vvdb-architecture

Honestly I'm not sure you can recommend switching at this point, this repository has not seen a commit in 11 months, it might be going out of support (we don't know).

@Havunen
Copy link

Havunen commented Feb 17, 2024

@Havunen
Copy link

Havunen commented Feb 17, 2024

If you still experience issues after trying out DotSwashbuckle, I might be able to fix those issues if you open separate issue for each of them with enough information and expected result defined

@harvzor
Copy link

harvzor commented Feb 22, 2024

I've been comparing Swashbuckle and NSwag here: https://github.com/harvzor/swashbuckle-vs-nswag

@Havunen I appreciate that someone is attempting to take over the reigns when it comes to Swashbuckle maintenance but I'm not sure if it's worth getting people to switch over to a new repo which has less than a week of development on it - it could become abandoned just like Swashbuckle 😬

I really wish Microsoft or some other large company would come in to take care of Swagger in dotnet

@martincostello
Copy link
Collaborator

FYI: #2778

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants