【问题标题】:How do I include subclasses in Swagger API documentation/ OpenAPI specification using Swashbuckle?如何使用 Swashbuckle 在 Swagger API 文档/OpenAPI 规范中包含子类?
【发布时间】:2015-12-21 13:55:12
【问题描述】:

我在 c# 中有一个 Asp.Net Web API 5.2 项目,并使用 Swashbuckle 生成文档。

我的模型包含继承,例如从 Animal 抽象类和 Dog 和 Cat 类派生的 Animal 属性。

Swashbuckle 仅显示 Animal 类的架构,因此我尝试使用 ISchemaFilter(他们也建议这样做),但我无法使其工作,也找不到合适的示例。

有人可以帮忙吗?

【问题讨论】:

  • 从今天开始,您应该考虑更新您的软件包。就我而言,我更新了最新的 NSwag.AspNetCore -Version 13.1.6(而不是 11.18.7)解决了这个问题。
  • 这在过去几年发生了很大变化,Swashbuckle 确实现在实现了多态性。

标签: c# api swagger subclassing openapi


【解决方案1】:

似乎 Swashbuckle 没有正确实现多态性,我理解作者关于子类作为参数的观点(如果一个动作需要一个 Animal 类并且如果你用狗对象或猫对象调用它时行为不同,那么您应该有 2 个不同的操作...)但作为返回类型,我认为返回 Animal 是正确的,并且对象可以是 Dog 或 Cat 类型。

因此,为了描述我的 API 并根据正确的准则生成正确的 JSON 模式(请注意我描述鉴别器的方式,如果您有自己的鉴别器,则可能需要特别更改该部分),我使用 document和模式过滤器如下:

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

前面的代码实现的是here,在“支持多态的模型”部分。它基本上产生如下内容:

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

【讨论】:

  • SchemaIdProvider 必须是你自己的班级?我发现您可以通过添加 Using Swashbuckle.Swagger 然后将该行代码更改为 var parentSchema = schemaRegistry.Definitions[abstractType.FriendlyId]; 来使用 Swagger 的默认约定
  • 是的,这是我的课。我需要它,因为我们还有一个 schemaId 的委托:configuration.SchemaId(SchemaIdProvider.GetSchemaId);
  • @PaoloVigori:我在 Swashbuckle.AspNetCore 上使用了它,PolymorphismDocumentFilter 被调用并在代码中设置了鉴别器,但在生成的 swagger 定义中没有。 allOf 条目在那里。有什么想法吗?
  • 克隆的模式不应该也复制model.AllOf吗?否则从派生类型派生的类型将没有任何属性。
【解决方案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】:

    this merge 到 Swashbuckle.AspNetCore,您可以通过以下方式获得对多态模式的基本支持:

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

    您还可以通过 Annotations 库中的属性来表达您的派生类型:

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

    This article 更详细地介绍了如何使用 Newtonsoft 反序列化派生类型。

    【讨论】:

    • 鉴别器是你的“类型”属性的名称吗?
    • @AndraAvram 这是要区分的属性的名称。在示例here 中,instrumentType 是判别器。
    • 这是最好的答案,IMO。使继承变得非常简单。此外,SwaggerSubTypes 现在已过时,取而代之的是每种类型的单独 SwaggerSubType 属性。
    【解决方案4】:

    我想跟进克雷格的回答。

    如果您使用 NSwag 使用 Paulo's answer 中解释并在 Craig's answer 中进一步增强的方法从 Swagger API 文档生成 TypeScript 定义,该文档由 Swashbuckle(撰写本文时为 3.x)生成,您可能会面临以下情况问题:

    1. 即使生成的类将扩展基类,生成的 TypeScript 定义也会有重复的属性。考虑以下 C# 类:

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

      使用上述答案时,IBaseClassIChildClass 接口的 TypeScript 定义将如下所示:

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

      如您所见,baseProperty 在基类和子类中定义不正确。为了解决这个问题,我们可以修改PolymorphismSchemaFilter&lt;T&gt; 类的Apply 方法以仅将拥有的属性包含到模式中,即从当前类型模式中排除继承的属性。这是一个例子:

      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 接口,有效地留下没有intermediateProperty 属性的IChildClass 的任何实例。

      我们可以使用下面的代码来解决这个问题:

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

    【讨论】:

    • 太棒了——正是我所需要的。非常感谢您的分享!
    • 在此之后我得到错误Could not resolve reference because of: Could not resolve pointer: /definitions/SuperClass does not exist in document,任何修复它的指针
    • @Sandy 您在配置模式过滤器时是否引用了 SuperClass,即 configuration.SchemaFilter&lt;PolymorphismSchemaFilter&lt;SuperClass&gt;&gt;();
    • @DejanJanjušević:是的,这就是问题所在,我在配置时并没有提到 SuperClass。感谢您的帮助。
    【解决方案5】:

    我们最近升级到 .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;
    

    【讨论】:

      【解决方案6】:

      这适用于版本 5.6.3:

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

      【讨论】:

        猜你喜欢
        • 2021-12-23
        • 2021-05-26
        • 1970-01-01
        • 2017-01-05
        • 1970-01-01
        • 2021-04-13
        • 2023-03-03
        • 2019-10-28
        • 1970-01-01
        相关资源
        最近更新 更多