Using Swashbuckle for Asp.net core how can I add a model to the generated model list?
Asked Answered
O

5

31

I'm using Swashbuckle with ASP.net core. It is producing a nice website with a list of models at the bottom.

enter image description here

How can I add a model to this list that isn't already appearing?

I return an abstract class in one of my requests and I want to list all the variations that inherit that abstract class.

Thanks in advance

Osmund answered 27/2, 2018 at 10:9 Comment(0)
W
53

You could create an document filter and register it globally.

public class CustomModelDocumentFilter<T> : IDocumentFilter where T : class
{
    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        context.SchemaRegistry.GetOrRegister(typeof(T));
    }
}

and then register it in your Startup class.

services.AddSwaggerGen(options =>
{
    ...
    options.DocumentFilter<CustomModelDocumentFilter<MyCustomModel>>();
    options.DocumentFilter<CustomModelDocumentFilter<MyOtherModel>>();
    ...
}

For polymorphic class you could use these to filters (slightly improved versions of this answer).

public class PolymorphismDocumentFilter<T> : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        RegisterSubClasses(context.SchemaRegistry, typeof(T));
    }

    private static void RegisterSubClasses(ISchemaRegistry schemaRegistry, Type abstractType)
    {
        const string discriminatorName = "$type";

        string friendlyId = abstractType.FriendlyId();
        if (!schemaRegistry.Definitions.TryGetValue(friendlyId, out Schema parentSchema))
            parentSchema = schemaRegistry.GetOrRegister(abstractType);

        // set up a discriminator property (it must be required)
        parentSchema.Discriminator = discriminatorName;
        parentSchema.Required = new List<string> { discriminatorName };

        if (parentSchema.Properties == null)
            parentSchema.Properties = new Dictionary<string, Schema>();

        if (!parentSchema.Properties.ContainsKey(discriminatorName))
            parentSchema.Properties.Add(discriminatorName, new Schema { Type = "string", Default = abstractType.FullName });

        // register all subclasses
        var derivedTypes = abstractType.GetTypeInfo().Assembly.GetTypes()
            .Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

        foreach (var item in derivedTypes)
            schemaRegistry.GetOrRegister(item);
    }
}

public class PolymorphismSchemaFilter<T> : ISchemaFilter
{
    private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init);

    public void Apply(Schema schema, SchemaFilterContext context)
    {
        if (!derivedTypes.Value.Contains(context.SystemType)) return;

        var type = context.SystemType;
        var clonedSchema = new Schema
        {
            Properties = schema.Properties,
            Type = schema.Type,
            Required = schema.Required
        };

        // schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in Swashbuckle.AspNetCore
        var parentSchema = new Schema { Ref = "#/definitions/" + typeof(T).Name };

        var assemblyName = Assembly.GetAssembly(type).GetName();
        schema.Discriminator = "$type";
        // This is required if you use Microsoft's AutoRest client to generate the JavaScript/TypeScript models
        schema.Extensions.Add("x-ms-discriminator-value", $"{type.FullName}, {assemblyName.Name}");
        schema.AllOf = new List<Schema> { parentSchema, clonedSchema };

        // reset properties for they are included in allOf, should be null but code does not handle it
        schema.Properties = new Dictionary<string, Schema>();
    }

    private static HashSet<Type> Init()
    {
        var abstractType = typeof(T);
        var dTypes = abstractType.GetTypeInfo().Assembly
            .GetTypes()
            .Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

        var result = new HashSet<Type>();

        foreach (var item in dTypes)
            result.Add(item);

        return result;
    }
}

Two filters are required. The first will add all of your delivered classes to the schema. It also adds properties that aren't existing int he base class to the derived type's schema.

The second filter adds some properties ($type for serialization when the model returns) and extensions (for Microsoft's AutoRest client / generator) as well as adding the allOf properties to the Swagger schema, which are required in order to create a inheritance schema when generated with swagger-gen or AutoRest.

Registration is similar, just that you need to register them in pairs (only registration of the base class is required)

// The following lines add polymorphism to the swagger.json schema, so that
// code generators can create properly inheritance hierarchies.
options.DocumentFilter<PolymorphismDocumentFilter<BaseClass>>();
options.SchemaFilter<PolymorphismSchemaFilter<BaseClass>>();

Update for ASP.NET Core 3 and Swashbuckle.AspNetCore 5.0

public class CustomModelDocumentFilter<T> : IDocumentFilter where T : class
{
    public void Apply(OpenApiDocument openapiDoc, DocumentFilterContext context)
    {
        context.SchemaGenerator.GenerateSchema(typeof(T), context.SchemaRepository);
    }
}

The PolymorphismDocumentFilter/PolymorphismSchemaFilter updated for Swashbuckle.AspNetCore 5.0

public class PolymorphismDocumentFilter<T> : IDocumentFilter
{
    public void Apply(OpenApiDocument openApiDoc, DocumentFilterContext context)
    {
        RegisterSubClasses(context, typeof(T));
    }

    private static void RegisterSubClasses(DocumentFilterContext context, Type abstractType)
    {
        const string discriminatorName = "$type";
        var schemaRepository = context.SchemaRepository.Schemas;
        var schemaGenerator = context.SchemaGenerator;

        if (!schemaRepository.TryGetValue(abstractType.Name, out OpenApiSchema parentSchema))
        {
            parentSchema = schemaGenerator.GenerateSchema(abstractType, context.SchemaRepository);
        }

        // set up a discriminator property (it must be required)
        parentSchema.Discriminator = new OpenApiDiscriminator { PropertyName = discriminatorName };
        parentSchema.Required.Add(discriminatorName);

        if (!parentSchema.Properties.ContainsKey(discriminatorName))
            parentSchema.Properties.Add(discriminatorName, new OpenApiSchema { Type = "string", Default = new OpenApiString(abstractType.FullName) });

        // register all subclasses
        var derivedTypes = abstractType.GetTypeInfo().Assembly.GetTypes()
            .Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

        foreach (var type in derivedTypes)
            schemaGenerator.GenerateSchema(type, context.SchemaRepository);
    }
}

and

public class PolymorphismSchemaFilter<T> : ISchemaFilter
{
    private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init);

    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        var type = context.ApiModel.Type;
        if (!derivedTypes.Value.Contains(type))
            return;

        var clonedSchema = new OpenApiSchema
        {
            Properties = schema.Properties,
            Type = schema.Type,
            Required = schema.Required
        };

        // schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle
        if(context.SchemaRepository.Schemas.TryGetValue(typeof(T).Name, out OpenApiSchema _))
        {
            schema.AllOf = new List<OpenApiSchema> {
                new OpenApiSchema { Reference = new OpenApiReference { Id = typeof(T).Name, Type = ReferenceType.Schema } },
                clonedSchema
            };
        }

        var assemblyName = Assembly.GetAssembly(type).GetName();
        schema.Discriminator = new OpenApiDiscriminator { PropertyName = "$type" };
        schema.AddExtension("x-ms-discriminator-value", new OpenApiString($"{type.FullName}, {assemblyName.Name}"));

        // reset properties for they are included in allOf, should be null but code does not handle it
        schema.Properties = new Dictionary<string, OpenApiSchema>();
    }

    private static HashSet<Type> Init()
    {
        var abstractType = typeof(T);
        var dTypes = abstractType.GetTypeInfo().Assembly
            .GetTypes()
            .Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

        var result = new HashSet<Type>();

        foreach (var item in dTypes)
            result.Add(item);

        return result;
    }
}
Wolff answered 27/2, 2018 at 13:8 Comment(4)
any idea how to do this in core 3 with swashbuckle 5? The new interface there does not have schemaregistry anymore, and not of the other items have something that does thisWeighting
Thanks this worked out great. :) with asp.net core and swashbuckle setup.Demoss
@SanketSonavane: Also updated with Swashbuckle.AspNetCore 5 examples, since it has a couple of breaking changes I had to go through a few weeks agoWolff
I know that I should not, but Thank you!Straub
P
4

Using this modified version of Tseng original answer for .NET Core 3+ and Shwashbuckle 5+

This allows you to apply [ApiExplorerSettings(GroupName = "my_group")] attributes to the manually mapped classes (models, schemas) to s them only in specific single swagger document.

using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

/// <summary>
/// 
/// </summary>
/// <typeparam name="T"></typeparam>
public class SwaggerDocumentFilter<T> : IDocumentFilter where T : class
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="openapiDoc"></param>
    /// <param name="context"></param>
    public void Apply(OpenApiDocument openapiDoc, DocumentFilterContext context)
    {
        var DocumentNames = typeof(T).GetCustomAttribute<ApiExplorerSettingsAttribute>();
        if (DocumentNames == null || !DocumentNames.GroupName.Any() || context.DocumentName == DocumentNames.GroupName)
        {
            context.SchemaGenerator.GenerateSchema(typeof(T), context.SchemaRepository);
        }
    }
}
Perfume answered 28/7, 2021 at 19:31 Comment(0)
S
3

Maybe not the cleanest solution but I achieved the same by setting a ProducesResponseType attribute above my Controller:

[ProducesResponseType(typeof(object), 200)]
public class FileController : Controller
{

Where you replace object with the object you want to display in the model and create a new line for each additional one. Just be sure you use a different Statuscode for each one, else it'll only display the last.

Schober answered 27/2, 2018 at 11:54 Comment(0)
D
3

I've had the same problem, where my models did not show up in swagger, because the return types of my functions had abstract type. I've modified the answer above, so that I could dump everything from a namespace into the model list.

In Startup I define this function:

public class GenericAPI_DocumentFilter<T> : IDocumentFilter where T : class
{

    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {

        foreach (var t in Assembly.GetExecutingAssembly().GetTypes())
        {
            if (t.Namespace.Contains("MyAPI") && t.IsClass)
            {

                var a = t.GetCustomAttribute(typeof(DataContractAttribute));
                if (a != null)
                {
                    context.SchemaRegistry.GetOrRegister(t);
                }

            }
        }

    }
}

In the swagger initialization I add one line:

services.AddSwaggerGen(opt =>
            {
...
opt.DocumentFilter<GenericAPI_DocumentFilter<object>>();
...
}
Dextrorotation answered 29/7, 2019 at 12:20 Comment(1)
That last line should read: opt.DocumentFilter<GenericAPI_DocumentFilter<object>>();Dextrorotation
D
1

In retrospect, the other answer that I found below (and on other pages) was better, namely to add this kind of attribute:

[HttpGet("")]
[ProducesResponseType(typeof(MyResult), (int)System.Net.HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorBase), (int)System.Net.HttpStatusCode.NotFound)]
... function definition ...

Why: because the function gives different types of objects based on different situations and with these attributes you can specify which objects are returned in which situation.

For example I can give back

return Ok(myresult)

or

return NotFound(myerror)

in my function based on whether I've found a result or not.

Dextrorotation answered 29/7, 2019 at 15:14 Comment(1)
Did this show you different objects under the same endpoint in the Swagger UI?Osmund

© 2022 - 2024 — McMap. All rights reserved.