简体   繁体   English

如何使用 Swashbuckle 在 Swagger API 文档/OpenAPI 规范中包含子类?

[英]How do I include subclasses in Swagger API documentation/ OpenAPI specification using Swashbuckle?

I have an Asp.Net web API 5.2 project in c# and generating documentation with Swashbuckle.我在 c# 中有一个 Asp.Net web API 5.2 项目,并使用 Swashbuckle 生成文档。

I have model that contain inheritance something like having an Animal property from an Animal abstract class and Dog and Cat classes that derive from it.我有一个包含继承的模型,例如具有来自 Animal 抽象类的 Animal 属性以及从它派生的 Dog 和 Cat 类。

Swashbuckle only shows the schema for the Animal class so I tried to play with ISchemaFilter (that what they suggest too) but I couldn't make it work and also I cannot find a proper example. Swashbuckle 仅显示 Animal 类的架构,因此我尝试使用 ISchemaFilter(他们也建议这样做)但我无法使其工作,而且我找不到合适的示例。

Anybody can help?有人可以帮忙吗?

It seems Swashbuckle doesn't implement polymorphism correctly and I understand the point of view of the author about subclasses as parameters (if an action expects an Animal class and behaves differently if you call it with a dog object or a cat object, then you should have 2 different actions...) but as return types I believe that it is correct to return Animal and the objects could be Dog or Cat types.看起来 Swashbuckle 没有正确实现多态性,我理解作者关于子类作为参数的观点(如果一个动作需要一个 Animal 类并且如果你用狗对象或猫对象调用它时表现不同,那么你应该有 2 个不同的操作...) 但作为返回类型,我认为返回 Animal 是正确的,并且对象可以是 Dog 或 Cat 类型。

So to describe my API and produce a proper JSON schema in line with correct guidelines (be aware of the way I describe the disciminator, if you have your own discriminator you may need to change that part in particular), I use document and schema filters as follows:因此,为了描述我的 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);
    }
}

What the previous code implements is specified here , in the section "Models with Polymorphism Support. It basically produces something like the following:前面的代码实现的内容在此处指定,在“具有多态性支持的模型”部分中。它基本上产生如下内容:

{
  "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"
          ]
        }
      ]
    }
  }
}

To follow on from Paulo's great answer, if you're using Swagger 2.0, you'll need to modify the classes as shown:要继续 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));
    }
}

As of this merge into Swashbuckle.AspNetCore, you can get basic support for polymorphic schemas by using:合并到 Swashbuckle.AspNetCore 开始,您可以使用以下方法获得对多态模式的基本支持:

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

You can also express your derived types via attributes present in the Annotations library:您还可以通过注释库中存在的属性来表达您的派生类型:

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

This article goes into further detail as to how you can deserialize derived types using Newtonsoft. 本文进一步详细介绍了如何使用 Newtonsoft 反序列化派生类型。

I'd like to follow up on Craig's answer.我想跟进克雷格的回答。

If you use NSwag to generate TypeScript definitions from the Swagger API documentation generated with Swashbuckle (3.x at the time of writing) using the method explained in Paulo's answer and further enhanced in Craig's answer you will probably face the following problems:如果您使用 NSwag 从 Swashbuckle(撰写本文时为 3.x)生成的 Swagger API 文档生成 TypeScript 定义,使用Paulo 的回答中解释并在Craig 的回答中进一步增强的方法,您可能会遇到以下问题:

  1. Generated TypeScript definitions will have duplicate properties even though the generated classes will extend the base classes.生成的 TypeScript 定义将具有重复的属性,即使生成的类将扩展基类。 Consider the following C# classes:考虑以下 C# 类:

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

    When using the aforementioned answers, the resulting TypeScript definition of IBaseClass and IChildClass interfaces would look like this:使用上述答案时, IBaseClassIChildClass接口的最终 TypeScript 定义如下所示:

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

    As you can see, the baseProperty is incorrectly defined in both base and child classes.如您所见, baseProperty在基类和子类中的定义都不正确。 To solve this, we can modify the Apply method of the PolymorphismSchemaFilter<T> class to include only owned properties to the schema, ie to exclude the inherited properties from the current types schema.为了解决这个问题,我们可以修改PolymorphismSchemaFilter<T>类的Apply方法,使其只包含模式中拥有的属性,即从当前类型模式中排除继承的属性。 Here is an example:这是一个例子:

     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. Generated TypeScript definitions will not reference properties from any existing intermediate abstract classes.生成的 TypeScript 定义不会引用任何现有中间抽象类的属性。 Consider the following C# classes:考虑以下 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; } }

    In this case, the generated TypeScript definitions would look like this:在这种情况下,生成的 TypeScript 定义将如下所示:

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

    Notice how the generated IChildClass interface extends ISuperClass directly, ignoring the IIntermediateClass interface, effectively leaving any instance of IChildClass without the intermediateProperty property.请注意生成的IChildClass接口如何直接扩展ISuperClass ,忽略IIntermediateClass接口,有效地使IChildClass的任何实例都没有intermediateProperty属性。

    We can use the following code to solve this problem:我们可以使用下面的代码来解决这个问题:

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

    This will ensure that the child class correctly references the intermediate class.这将确保子类正确引用中间类。

In conclusion, the final code would then look like this:总之,最终代码将如下所示:

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

We recently upgraded to .NET Core 3.1 and Swashbuckle.AspNetCore 5.0 And the API is somewhat changed.我们最近升级到 .NET Core 3.1 和 Swashbuckle.AspNetCore 5.0 并且 API 有所改变。 In case somebody needs this filter here is the code with minimal changes to get similar behavior:万一有人需要这个过滤器,这里是通过最少的更改获得类似行为的代码:

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

I did not inspect the result fully, but it seems that it gives the same behavior.我没有完全检查结果,但它似乎给出了相同的行为。

Also note that you need to import these namespaces:另请注意,您需要导入这些命名空间:

using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Any;
using System.Reflection;
using Swashbuckle.AspNetCore.SwaggerGen;

This works as of version 5.6.3:这适用于版本 5.6.3:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM