Как включить подклассы в документацию Swagger API/спецификацию OpenAPI, используя Swashbuckle?

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

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

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

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

Ответ 1

Кажется, 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"
          ]
        }
      ]
    }
  }
}

Ответ 2

Чтобы продолжить из Paulo отличный ответ, если вы используете 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));
    }
}

Ответ 3

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

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

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

    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 не будут ссылаться на свойства любых существующих промежуточных абстрактных классов. Рассмотрим следующие классы С#:

    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>();
    }

Ответ 4

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

В нашем методе возвращаем Error и BusinessIntelligenceItemDTO List из метода Get, а error также из того же метода.

Swashbuckle показывает только схему для [ResponseType (typeof (BusinessIntelligenceItemDTO))]], но не для класса ошибок

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