Swashbuckle:多态性不适用于外部 nuget 包

Swashbuckle: Polymorphism not working with external nuget package

在我们的 API 中,我们希望在用户调用端点时 return 来自外部 Nuget 包的对象。

这个对象(Can be viewed here) has a couple of properties. One of them is called Action. This property has as type IPaymentResponseAction but can be a set of different action types (You can see them all over here)。

生成的 swagger 不知道这些操作,也不会生成所需的代码。即使设置了多态性设置。

    services.AddSwaggerGen(c =>
            {
                c.EnableAnnotations();
                c.UseOneOfForPolymorphism();
            });

有没有办法让这些对象在我的招摇中出现?也许有一些习惯 SwaggerGenOptions?


使用 c.SelectSubTypesUsing 代码回答第一个问题后更新

    Adyen.Model.Checkout.PaymentResponse": {
        "type": "object",
        "properties": {
          "resultCode": {
            "$ref": "#/components/schemas/Adyen.Model.Checkout.PaymentResponse.ResultCodeEnum"
          },
          "action": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.IPaymentResponseAction"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutAwaitAction"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutDonationAction"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutOneTimePasscodeAction"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutQrCodeAction"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutRedirectAction"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutSDKAction"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutThreeDS2Action"
              },
              {
                "$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutVoucherAction"
              }
            ],
            "nullable": true
          }......

IPaymentResponseAction 是:

    "Adyen.Model.Checkout.Action.IPaymentResponseAction": {
        "required": [
          "type"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "await": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutAwaitAction",
            "donation": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutDonationAction",
            "oneTimePasscode": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutOneTimePasscodeAction",
            "qrCode": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutQrCodeAction",
            "redirect": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutRedirectAction",
            "sdk": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutSDKAction",
            "threeDS2Action": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutThreeDS2Action",
            "voucher": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutVoucherAction"
          }
        }
      },

更新: 我所有的动作现在看起来都是这样,所以我认为它还没有。但它接近了!

    "CheckoutAwaitAction": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/Rig.Commercial.Reservation.Core.Settings.Swagger.Swagger_Models.PaymentResponseAction"
          }
        ],
        "additionalProperties": false
      }

更新答案

这是解决问题的更新答案:) 很抱歉 post。

您描述的问题是由于 Swashbuckle 缺乏处理 C# 接口以反映多态层次结构的能力引起的(对我来说似乎是缺少的功能)。

这是一个解决方法(参见 MVP 项目 here)。

第 1 步。Swashbuckle 个选项

    c.EnableAnnotations(enableAnnotationsForInheritance: true, enableAnnotationsForPolymorphism: true);
    c.UseAllOfToExtendReferenceSchemas();
    c.UseAllOfForInheritance();
    c.UseOneOfForPolymorphism();

第 2 步。鉴别器选项

Swashbuckle 不将接口视为“父”类型。如果我们让它“认为”它仍在处理 class 而不是接口怎么办?介绍一下 PaymentResponseAction class:

    [DataContract]
    [SwaggerDiscriminator("type")]
    public class PaymentResponseAction : IPaymentResponseAction
    {
        [JsonProperty(PropertyName = "type")]
        public string Type { get; set; }
    }

AddSwaggerGen调用中,我们还应该提供正确的鉴别器选项:

    c.SelectDiscriminatorNameUsing(type =>
    {
        return type.Name switch
        {
            nameof(PaymentResponseAction) => "type",
            _ => null
        };
    });

    c.SelectDiscriminatorValueUsing(subType =>
    {
        return subType.Name switch
        {
            nameof(CheckoutAwaitAction) => "await",
            nameof(CheckoutBankTransferAction) => "bank",
            nameof(CheckoutDonationAction) => "donation",
            nameof(CheckoutOneTimePasscodeAction) => "oneTimePasscode",
            // rest of the action types ...
            _ => null
        };
    });

第 3 步。allOf 实施中的关键字 classes

至此,几乎一切正常。唯一缺少的是实现 classes 的 allOf 关键字。目前,它不可能只使用 Swashbuckle 的选项,因为它在构造 allOf.

时使用 BaseType 来解析子类型

和以前一样,我们可以 Swashbuckle 认为它处理继承类型。我们可以生成继承我们新 PaymentResponseAction class 的“假”类型,并从我们感兴趣的实现类型中复制属性。这些“假”类型不必是功能性的;它们应该包含足够的类型信息以使 Swashbuckle 满意。

这是执行此操作的方法示例。它接受一个源类型来从一个基本类型和 returns 一个新类型复制属性。它还 copies custom attributes 可以很好地与 AddSwaggerGenNewtonsoftSupport.

等依赖设置一起使用

请注意,应该改进此代码以使其可以投入生产;例如,它不应该“复制”具有 JsonIgnore 或类似属性的 public 属性。

    private static Type GenerateReparentedType(Type originalType, Type parent)
    {
        var assemblyBuilder =
            AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("hack"), AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("hack");
        var typeBuilder = moduleBuilder.DefineType(originalType.Name, TypeAttributes.Public, parent);

        foreach (var property in originalType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
        {
            var newProperty = typeBuilder
                .DefineProperty(property.Name, property.Attributes, property.PropertyType, null);

            var getMethod = property.GetMethod;
            if (getMethod is not null)
            {
                var getMethodBuilder = typeBuilder
                    .DefineMethod(getMethod.Name, getMethod.Attributes, getMethod.ReturnType, Type.EmptyTypes);
                getMethodBuilder.GetILGenerator().Emit(OpCodes.Ret);
                newProperty.SetGetMethod(getMethodBuilder);
            }

            var setMethod = property.SetMethod;
            if (setMethod is not null)
            {
                var setMethodBuilder = typeBuilder
                    .DefineMethod(setMethod.Name, setMethod.Attributes, setMethod.ReturnType, Type.EmptyTypes);
                setMethodBuilder.GetILGenerator().Emit(OpCodes.Ret);
                newProperty.SetSetMethod(setMethodBuilder);
            }

            var customAttributes = CustomAttributeData.GetCustomAttributes(property).ToArray();
            foreach (var customAttributeData in customAttributes)
            {
                newProperty.SetCustomAttribute(DefineCustomAttribute(customAttributeData));
            }
        }

        var type = typeBuilder.CreateType();
        return type ?? throw new InvalidOperationException($"Unable to generate a re-parented type for {originalType}.");
    }

    private static CustomAttributeBuilder DefineCustomAttribute(CustomAttributeData attributeData)
    {
        // based on 

        var constructorArguments = attributeData.ConstructorArguments
            .Select(argument => argument.Value)
            .ToArray();

        var propertyArguments = new List<PropertyInfo>();
        var propertyArgumentValues = new List<object?>();
        var fieldArguments = new List<FieldInfo>();
        var fieldArgumentValues = new List<object?>();

        foreach (var argument in attributeData.NamedArguments ?? Array.Empty<CustomAttributeNamedArgument>())
        {
            var fieldInfo = argument.MemberInfo as FieldInfo;
            var propertyInfo = argument.MemberInfo as PropertyInfo;

            if (fieldInfo != null)
            {
                fieldArguments.Add(fieldInfo);
                fieldArgumentValues.Add(argument.TypedValue.Value);
            }
            else if (propertyInfo != null)
            {
                propertyArguments.Add(propertyInfo);
                propertyArgumentValues.Add(argument.TypedValue.Value);
            }
        }

        return new CustomAttributeBuilder(
            attributeData.Constructor, constructorArguments,
            propertyArguments.ToArray(), propertyArgumentValues.ToArray(),
            fieldArguments.ToArray(), fieldArgumentValues.ToArray()
        );
    }

现在我们可以在 AddSwaggerGen 调用中使用它来使 Swashbuckle 以我们想要的方式解析这些类型:

    var actionTypes = new[]
    {
        GenerateReparentedType(typeof(CheckoutAwaitAction), typeof(PaymentResponseAction)),
        GenerateReparentedType(typeof(CheckoutBankTransferAction), typeof(PaymentResponseAction)),
        GenerateReparentedType(typeof(CheckoutDonationAction), typeof(PaymentResponseAction)),
        GenerateReparentedType(typeof(CheckoutOneTimePasscodeAction), typeof(PaymentResponseAction)),
        // rest of the action types ...
    };

    c.SelectSubTypesUsing(type =>
    {
        var allTypes = typeof(Startup).Assembly.GetTypes().ToArray();
        return type.Name switch
        {
            nameof(PaymentResponseAction) => new[] { typeof(PaymentResponseAction) }.Union(actionTypes),
            nameof(IPaymentResponseAction) => new[] { typeof(PaymentResponseAction) }.Union(actionTypes),
            _ => allTypes.Where(t => t.IsSubclassOf(type))
        };
    });

结果

现在 Swashbuckle 应该可以正确生成所有内容:

paths:
  /api/someEndpoint:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentResponse'
# ...

components:
  schemas:
    PaymentResponse:
      type: object
      properties:
        resultCode:
          allOf:
            - $ref: '#/components/schemas/ResultCodeEnum'
          nullable: true
        action:
          oneOf:
            - $ref: '#/components/schemas/PaymentResponseAction'
            - $ref: '#/components/schemas/CheckoutAwaitAction'
            - $ref: '#/components/schemas/CheckoutBankTransferAction'
            - $ref: '#/components/schemas/CheckoutDonationAction'
            - $ref: '#/components/schemas/CheckoutOneTimePasscodeAction'
            # ... rest of the actions
          nullable: true
        # ... rest of the properties
    PaymentResponseAction:
      required:
        - type
      type: object
      properties:
        type:
          type: string
          nullable: true
      additionalProperties: false
      discriminator:
        propertyName: type
        mapping:
          await: '#/components/schemas/CheckoutAwaitAction'
          bank: '#/components/schemas/CheckoutBankTransferAction'
          donation: '#/components/schemas/CheckoutDonationAction'
          oneTimePasscode: '#/components/schemas/CheckoutOneTimePasscodeAction'
          # ... rest of the action mapping
    CheckoutAwaitAction:
      type: object
      allOf:
        - $ref: '#/components/schemas/PaymentResponseAction'
      properties:
        # CheckoutAwaitAction's own properties
      additionalProperties: false
    CheckoutBankTransferAction:
      type: object
      allOf:
        - $ref: '#/components/schemas/PaymentResponseAction'
      properties:
        # CheckoutBankTransferAction's own properties
      additionalProperties: false
    CheckoutDonationAction:
      type: object
      allOf:
        - $ref: '#/components/schemas/PaymentResponseAction'
      properties:
        # CheckoutDonationAction's own properties
      additionalProperties: false
    CheckoutOneTimePasscodeAction:
      type: object
      allOf:
        - $ref: '#/components/schemas/PaymentResponseAction'
      properties:
        # CheckoutOneTimePasscodeAction's own properties
      additionalProperties: false
    # ... rest of the action classes

上一个(不完整的)答案

这可以使用 Swashbuckle.AspNetCore.Annotations package 来完成。根据 API 设计,您可以使用以下方法之一。

响应模式不依赖于响应代码

此方法利用了在响应模式中使用 oneOf 的优势。这个想法是让 Swashbuckle 生成一个具有 oneOf:

的响应模式
responses:
  '200':
    description: Success
    content:
      application/json:
        schema:
          oneOf:
            - $ref: '#/components/schemas/CheckoutAwaitAction'
            - $ref: '#/components/schemas/CheckoutBankTransferAction'
            - $ref: '#/components/schemas/CheckoutDonationAction'
            - $ref: '#/components/schemas/CheckoutOneTimePasscodeAction'
            # ...

这是您需要做的:

  1. UseOneOfForPolymorphismSelectSubTypesUsing 选项添加到您的 AddSwaggerGen 调用中;确保您的 SelectSubTypesUsingIPaymentResponseAction 接口解析为您的 API 从控制器方法返回的所有所需实现:

    services.AddSwaggerGen(c =>
        {
        // ...
    
        c.UseOneOfForPolymorphism();
        c.SelectSubTypesUsing(baseType =>
        {
            if (baseType == typeof(IPaymentResponseAction))
            {
                return new[]
                {
                    typeof(CheckoutAwaitAction),
                    typeof(CheckoutBankTransferAction),
                    typeof(CheckoutDonationAction),
                    typeof(CheckoutOneTimePasscodeAction),
                    // ...
                };
            }
    
            return Enumerable.Empty<Type>();
        });
    
    
  2. 向您的控制器方法添加 SwaggerResponse 注释。仅指定 IPaymentResponseAction 接口。

    [HttpGet]
    [SwaggerResponse((int)HttpStatusCode.OK, "response description", typeof(IPaymentResponseAction))]
    public IPaymentResponseAction GetPaymentAction()
    {
        // ...
    
    

这将在 Swagger-UI:

中为您提供所需的架构

Response in Swagger-UI

请注意 Swagger-UI 如果架构具有 oneOf 定义,“示例值”部分:它将只显示第一个已解析类型的响应示例SelectSubTypesUsing呼唤。

响应模式取决于响应代码

这看起来不像你的情况,但我仍然想将其作为一个选项提及。

如果不同的响应码响应模式不同,可以直接在controller中指定对应的类型:

[HttpPost]
[SwaggerResponse((int)HttpStatusCode.Created, "response description", typeof(CheckoutAwaitAction))]
[SwaggerResponse((int)HttpStatusCode.OK, "response description", typeof(CheckoutBankTransferAction))]
// ...
public IPaymentResponseAction PostPaymentAction()
{
    // ...