Как включить подклассы в документацию Swagger API с помощью Swashbuckle?

У меня есть проект Asp.Net web API 5.2 в C# и создание документации с помощью Swashbuckle.

У меня есть модель, которая содержит наследование, что-то вроде наличия свойства Animal от абстрактного класса Animal и классов Dog и Cat, которые вытекают из него.

Swashbuckle показывает только схему для класса Animal, поэтому я попытался поиграть с ISchemaFilter (то, что они предлагают тоже), но я не смог заставить его работать, а также я не могу найти подходящий пример.

Кто-нибудь может помочь?

6 ответов

Решение

Кажется, Swashbuckle не реализует полиморфизм правильно, и я понимаю точку зрения автора относительно подклассов как параметров (если действие определяет класс Animal и ведет себя по-другому, если вы вызываете его с помощью объекта dog или объекта cat, то вам следует есть 2 разных действия..) но в качестве возвращаемых типов я считаю правильным возвращать Animal и объекты могут быть типа Dog или Cat.

Поэтому, чтобы описать мой API и создать правильную схему JSON в соответствии с правильными указаниями (помните, как я описываю дисклиминатор, если у вас есть собственный дискриминатор, вам, возможно, потребуется изменить эту часть в частности), я использую фильтры документов и схем следующим образом:

SwaggerDocsConfig configuration;
.....
configuration.DocumentFilter<PolymorphismDocumentFilter<YourBaseClass>>();
configuration.SchemaFilter<PolymorphismSchemaFilter<YourBaseClass>>();
.....

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

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

        var result = new HashSet<Type>();

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

        return result;
    }

    public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
    {
        if (!derivedTypes.Value.Contains(type)) return;

        var clonedSchema = new Schema
                                {
                                    properties = schema.properties,
                                    type = schema.type,
                                    required = schema.required
                                };

        //schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle
        var parentSchema = new Schema { @ref = "#/definitions/" + typeof(T).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>();
    }
}

public class PolymorphismDocumentFilter<T> : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, System.Web.Http.Description.IApiExplorer apiExplorer)
    {
        RegisterSubClasses(schemaRegistry, typeof(T));
    }

    private static void RegisterSubClasses(SchemaRegistry schemaRegistry, Type abstractType)
    {
        const string discriminatorName = "discriminator";

        var parentSchema = schemaRegistry.Definitions[SchemaIdProvider.GetSchemaId(abstractType)];

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

        if (!parentSchema.properties.ContainsKey(discriminatorName))
            parentSchema.properties.Add(discriminatorName, new Schema { type = "string" });

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

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

То, что реализует предыдущий код, указано здесь, в разделе "Модели с поддержкой полиморфизма". По сути, оно производит что-то вроде следующего:

{
  "definitions": {
    "Pet": {
      "type": "object",
      "discriminator": "petType",
      "properties": {
        "name": {
          "type": "string"
        },
        "petType": {
          "type": "string"
        }
      },
      "required": [
        "name",
        "petType"
      ]
    },
    "Cat": {
      "description": "A representation of a cat",
      "allOf": [
        {
          "$ref": "#/definitions/Pet"
        },
        {
          "type": "object",
          "properties": {
            "huntingSkill": {
              "type": "string",
              "description": "The measured skill for hunting",
              "default": "lazy",
              "enum": [
                "clueless",
                "lazy",
                "adventurous",
                "aggressive"
              ]
            }
          },
          "required": [
            "huntingSkill"
          ]
        }
      ]
    },
    "Dog": {
      "description": "A representation of a dog",
      "allOf": [
        {
          "$ref": "#/definitions/Pet"
        },
        {
          "type": "object",
          "properties": {
            "packSize": {
              "type": "integer",
              "format": "int32",
              "description": "the size of the pack the dog is from",
              "default": 0,
              "minimum": 0
            }
          },
          "required": [
            "packSize"
          ]
        }
      ]
    }
  }
}

После этого слияния с Swashbuckle.AspNetCore вы можете получить базовую поддержку полиморфных схем, используя:

services.AddSwaggerGen(c =>
{
    c.GeneratePolymorphicSchemas();
}

Вы также можете выразить свои производные типы через атрибуты, присутствующие в библиотеке аннотаций:

[SwaggerSubTypes(typeof(SubClass), Discriminator = "value")]

В этой статье более подробно рассказывается о том, как можно десериализовать производные типы с помощью Newtonsoft.

Чтобы следовать из отличного ответа Пауло, если вы используете Swagger 2.0, вам нужно изменить классы, как показано:

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

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

        var result = new HashSet<Type>();

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

        return result;
    }

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

        var clonedSchema = new Schema
        {
            Properties = model.Properties,
            Type = model.Type,
            Required = model.Required
        };

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

        model.AllOf = new List<Schema> { parentSchema, clonedSchema };

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

public class PolymorphismDocumentFilter<T> : IDocumentFilter
{
    private static void RegisterSubClasses(ISchemaRegistry schemaRegistry, Type abstractType)
    {
        const string discriminatorName = "discriminator";

        var parentSchema = schemaRegistry.Definitions[abstractType.Name];

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

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

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

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

    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        RegisterSubClasses(context.SchemaRegistry, typeof(T));
    }
}

Я хотел бы продолжить ответ Крейга.

Если вы используете NSwag для генерации определений TypeScript из документации Swagger API, сгенерированной с помощью Swashbuckle (3.x на момент написания) с использованием метода, описанного в ответе Пауло и дополнительно усовершенствованного в ответе Крейга, вы, вероятно, столкнетесь со следующими проблемами:

  1. Сгенерированные определения TypeScript будут иметь повторяющиеся свойства, даже если сгенерированные классы расширят базовые классы. Рассмотрим следующие классы C#:

    public abstract class BaseClass
    {
        public string BaseProperty { get; set; }
    }
    
    public class ChildClass : BaseClass
    {
        public string ChildProperty { get; set; }
    }
    

    При использовании вышеупомянутых ответов результирующее определение TypeScript IBaseClass а также IChildClass Интерфейсы будут выглядеть так:

    export interface IBaseClass {
        baseProperty : string | undefined;
    }
    
    export interface IChildClass extends IBaseClass {
        baseProperty : string | undefined;
        childProperty: string | undefined;
    }
    

    Как видите, baseProperty неправильно определен как в базовых, так и в дочерних классах. Чтобы решить эту проблему, мы можем изменить Apply метод PolymorphismSchemaFilter<T> класс для включения в схему только собственных свойств, т. е. для исключения унаследованных свойств из схемы текущих типов. Вот пример:

    public void Apply(Schema model, SchemaFilterContext context)
    {
        ...
    
        // Prepare a dictionary of inherited properties
        var inheritedProperties = context.SystemType.GetProperties()
            .Where(x => x.DeclaringType != context.SystemType)
            .ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
    
        var clonedSchema = new Schema
        {
            // Exclude inherited properties. If not excluded, 
            // they would have appeared twice in nswag-generated typescript definition
            Properties =
                model.Properties.Where(x => !inheritedProperties.ContainsKey(x.Key))
                    .ToDictionary(x => x.Key, x => x.Value),
            Type = model.Type,
            Required = model.Required
        };
    
        ...
    }
    
  2. Сгенерированные определения TypeScript не будут ссылаться на свойства любых существующих промежуточных абстрактных классов. Рассмотрим следующие классы C#:

    public abstract class SuperClass
    {
        public string SuperProperty { get; set; }
    }
    
    public abstract class IntermediateClass : SuperClass
    {
         public string IntermediateProperty { get; set; }
    }
    
    public class ChildClass : BaseClass
    {
        public string ChildProperty { get; set; }
    }
    

    В этом случае сгенерированные определения TypeScript будут выглядеть так:

    export interface ISuperClass {
        superProperty: string | undefined;
    }        
    
    export interface IIntermediateClass extends ISuperClass {
        intermediateProperty : string | undefined;
    }
    
    export interface IChildClass extends ISuperClass {
        childProperty: string | undefined;
    }
    

    Обратите внимание, как генерируется IChildClass интерфейс расширяется ISuperClass напрямую, игнорируя IIntermediateClass интерфейс, эффективно оставляя любой экземпляр IChildClass без intermediateProperty имущество.

    Мы можем использовать следующий код для решения этой проблемы:

    public void Apply(Schema model, SchemaFilterContext context)
    {
        ...
    
        // Use the BaseType name for parentSchema instead of typeof(T), 
        // because we could have more classes in the hierarchy
        var parentSchema = new Schema
        {
            Ref = "#/definitions/" + (context.SystemType.BaseType?.Name ?? typeof(T).Name)
        };
    
        ...
    }
    

    Это гарантирует, что дочерний класс правильно ссылается на промежуточный класс.

В заключение, итоговый код будет выглядеть так:

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

        // Prepare a dictionary of inherited properties
        var inheritedProperties = context.SystemType.GetProperties()
            .Where(x => x.DeclaringType != context.SystemType)
            .ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);

        var clonedSchema = new Schema
        {
            // Exclude inherited properties. If not excluded, 
            // they would have appeared twice in nswag-generated typescript definition
            Properties =
                model.Properties.Where(x => !inheritedProperties.ContainsKey(x.Key))
                    .ToDictionary(x => x.Key, x => x.Value),
            Type = model.Type,
            Required = model.Required
        };

        // Use the BaseType name for parentSchema instead of typeof(T), 
        // because we could have more abstract classes in the hierarchy
        var parentSchema = new Schema
        {
            Ref = "#/definitions/" + (context.SystemType.BaseType?.Name ?? typeof(T).Name)
        };
        model.AllOf = new List<Schema> { parentSchema, clonedSchema };

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

Это работает с версии 5.6.3:

      services.AddSwaggerGen(options =>
{
    options.UseOneOfForPolymorphism();
    options.SelectDiscriminatorNameUsing(_ => "type");
});  

Недавно мы обновились до.NET Core 3.1 и Swashbuckle.AspNetCore 5.0. И API несколько изменился. Если кому-то нужен этот фильтр, вот код с минимальными изменениями для получения аналогичного поведения:

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

    private static void RegisterSubClasses(SchemaRepository schemaRegistry, ISchemaGenerator schemaGenerator, Type abstractType)
    {
        const string discriminatorName = "$type";
        OpenApiSchema parentSchema = null;

        if (schemaRegistry.TryGetIdFor(abstractType, out string parentSchemaId))
            parentSchema = schemaRegistry.Schemas[parentSchemaId];
        else
            parentSchema = schemaRegistry.GetOrAdd(abstractType, parentSchemaId, () => new OpenApiSchema());

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

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

        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 item in derivedTypes)
            schemaGenerator.GenerateSchema(item, schemaRegistry);
    }
}

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

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

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

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

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

        // 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;
    }
}

Я не проверял результат полностью, но похоже, что он дает такое же поведение.

Также обратите внимание, что вам необходимо импортировать эти пространства имен:

using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Any;
using System.Reflection;
using Swashbuckle.AspNetCore.SwaggerGen;
Другие вопросы по тегам