将 ISchemaFilter 示例过渡到 Swashbuckle 5.0

Transitioning ISchemaFilter examples to Swashbuckle 5.0

在 Swashbuckle 5 之前,可以定义和注册一个 ISchemaFilter 来提供模型的示例实现:

public class MyModelExampleSchemaFilter : ISchemaFilter
{
    public void Apply(Schema schema, SchemaFilterContext context)
    {
        if (context.SystemType.IsAssignableFrom(typeof(MyModel)))
        {
            schema.Example = new MyModel
            {
                Name = "model name",
                value = 42
            };
        }
    }
}

Schema.Example 会采用任意对象,并且在生成 OpenApi 架构时会正确序列化。

但是,随着迁移到 .NET Core 3 和 Swashbuckle 5,Schema.Example 属性 不再是 object 并且需要类型 Microsoft.OpenApi.Any.IOpenApiAny。似乎没有关于如何提供新示例的记录路径。

基于查看 Microsoft.OpenApi 中的代码,我尝试构建我自己的 IOpenApiAny 实现,但是任何使用它生成示例的尝试都在 [=19] 中失败=] 在其 Write 方法甚至被调用之前。我并不是说下面的代码是完全正确的,但我希望它至少能指明一条道路以及如何前进。

/// <summary>
///     A class that recursively adapts a unidirectional POCO tree into an <see cref="IOpenApiAny" />
/// </summary>
/// <remarks>
///     <para>This will fail if a graph is provided (backwards and forwards references</para>
/// </remarks>
public class OpenApiPoco : IOpenApiAny
{
    /// <summary>
    ///     The model to be converted
    /// </summary>
    private readonly object _model;

    /// <summary>
    ///     Initializes a new instance of the <see cref="OpenApiPoco" /> class.
    /// </summary>
    /// <param name="model">the model to convert to an <see cref="IOpenApiAny" /> </param>
    public OpenApiPoco(object model)
    {
        this._model = model;
    }

    /// <inheritdoc />
    public AnyType AnyType => DetermineAnyType(this._model);

    #region From Interface IOpenApiExtension

    /// <inheritdoc />
    public void Write(IOpenApiWriter writer,
                        OpenApiSpecVersion specVersion)
    {
        this.Write(this._model, writer, specVersion);
    }

    #endregion

    private static AnyType DetermineAnyType(object model)
    {
        if (model is null)
        {
            return AnyType.Null;
        }

        var modelType = model.GetType();

        if (modelType.IsAssignableFrom(typeof(int))
            || modelType.IsAssignableFrom(typeof(long))
            || modelType.IsAssignableFrom(typeof(float))
            || modelType.IsAssignableFrom(typeof(double))
            || modelType.IsAssignableFrom(typeof(string))
            || modelType.IsAssignableFrom(typeof(byte))
            || modelType.IsAssignableFrom(typeof(byte[])) // Binary or Byte
            || modelType.IsAssignableFrom(typeof(bool))
            || modelType.IsAssignableFrom(typeof(DateTimeOffset)) // DateTime
            || modelType.IsAssignableFrom(typeof(DateTime))       // Date
        )
        {
            return AnyType.Primitive;
        }

        if (modelType.IsAssignableFrom(typeof(IEnumerable))) // test after primitive check so as to avoid catching string and byte[]
        {
            return AnyType.Array;
        }

        return AnyType.Object; // Assume object
    }

    private void Write(object model,
                        [NotNull] IOpenApiWriter writer,
                        OpenApiSpecVersion specVersion)
    {
        if (writer is null)
        {
            throw new ArgumentNullException(nameof(writer));
        }

        if (model is null)
        {
            writer.WriteNull();
            return;
        }

        var modelType = model.GetType();

        if (modelType.IsAssignableFrom(typeof(int))
            || modelType.IsAssignableFrom(typeof(long))
            || modelType.IsAssignableFrom(typeof(float))
            || modelType.IsAssignableFrom(typeof(double))
            || modelType.IsAssignableFrom(typeof(string))
            || modelType.IsAssignableFrom(typeof(byte[])) // Binary or Byte
            || modelType.IsAssignableFrom(typeof(bool))
            || modelType.IsAssignableFrom(typeof(DateTimeOffset)) // DateTime
            || modelType.IsAssignableFrom(typeof(DateTime))       // Date
        )
        {
            this.WritePrimitive(model, writer, specVersion);
            return;
        }

        if (modelType.IsAssignableFrom(typeof(IEnumerable))) // test after primitive check so as to avoid catching string and byte[]
        {
            this.WriteArray((IEnumerable) model, writer, specVersion);
            return;
        }

        this.WriteObject(model, writer, specVersion); // Assume object
    }

    private void WritePrimitive(object model,
                                IOpenApiWriter writer,
                                OpenApiSpecVersion specVersion)
    {
        switch (model.GetType())
        {
            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(string)): // string
                writer.WriteValue((string) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(byte[])): // assume Binary; can't differentiate from Byte and Binary based on type alone
                // if we chose to treat byte[] as Byte we would Base64 it to string. eg: writer.WriteValue(Convert.ToBase64String((byte[]) propertyValue));
                writer.WriteValue(Encoding.UTF8.GetString((byte[]) model));
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(bool)): // boolean
                writer.WriteValue((bool) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(DateTimeOffset)): // DateTime as DateTimeOffset
                writer.WriteValue((DateTimeOffset) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(DateTime)): // Date as DateTime
                writer.WriteValue((DateTime) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(double)): // Double
                writer.WriteValue((double) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(float)): // Float
                writer.WriteValue((float) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(int)): // Integer
                writer.WriteValue((int) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(long)): // Long
                writer.WriteValue((long) model);
                break;

            case TypeInfo typeInfo
                when typeInfo.IsAssignableFrom(typeof(Guid)): // Guid (as a string)
                writer.WriteValue(model.ToString());
                break;

            default:
                throw new ArgumentOutOfRangeException(nameof(model),
                                                        model?.GetType()
                                                            .Name,
                                                        "unexpected model type");
        }
    }

    private void WriteArray(IEnumerable model,
                            IOpenApiWriter writer,
                            OpenApiSpecVersion specVersion)
    {
        writer.WriteStartArray();

        foreach (var item in model)
        {
            this.Write(item, writer, specVersion); // recursive call
        }

        writer.WriteEndArray();
    }

    private void WriteObject(object model,
                                IOpenApiWriter writer,
                                OpenApiSpecVersion specVersion)
    {
        var propertyInfos = model.GetType()
                                    .GetProperties();

        writer.WriteStartObject();

        foreach (var property in propertyInfos)
        {
            writer.WritePropertyName(property.Name);

            var propertyValue = property.GetValue(model);

            switch (propertyValue.GetType())
            {
                case TypeInfo typeInfo                                        // primitives
                    when typeInfo.IsAssignableFrom(typeof(string))            // string
                            || typeInfo.IsAssignableFrom(typeof(byte[]))         // assume Binary or Byte
                            || typeInfo.IsAssignableFrom(typeof(bool))           // boolean
                            || typeInfo.IsAssignableFrom(typeof(DateTimeOffset)) // DateTime as DateTimeOffset
                            || typeInfo.IsAssignableFrom(typeof(DateTime))       // Date as DateTime
                            || typeInfo.IsAssignableFrom(typeof(double))         // Double
                            || typeInfo.IsAssignableFrom(typeof(float))          // Float
                            || typeInfo.IsAssignableFrom(typeof(int))            // Integer
                            || typeInfo.IsAssignableFrom(typeof(long))           // Long
                            || typeInfo.IsAssignableFrom(typeof(Guid)):          // Guid (as a string)
                    this.WritePrimitive(propertyValue, writer, specVersion);
                    break;

                case TypeInfo typeInfo                                   // Array test after primitive check so as to avoid catching string and byte[]
                    when typeInfo.IsAssignableFrom(typeof(IEnumerable)): // Enumerable as array of objects
                    this.WriteArray((IEnumerable) propertyValue, writer, specVersion);
                    break;

                case TypeInfo typeInfo                              // object
                    when typeInfo.IsAssignableFrom(typeof(object)): // Object
                default:
                    this.Write(propertyValue, writer, specVersion); // recursive call
                    break;
            }
        }

        writer.WriteEndObject();
    }
}

ISchemaFilter 示例转换为 Swashbuckle 5.0 以遵守适当的序列化规则的正确方法是什么?

他们在 repo 上有一个例子:
https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/9bb9be9b318c576d236152f142aafa8c860fb946/test/WebSites/Basic/Swagger/ExamplesSchemaFilter.cs#L8

public class ExamplesSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        schema.Example = GetExampleOrNullFor(context.Type);
    }

    private IOpenApiAny GetExampleOrNullFor(Type type)
    {
        switch (type.Name)
        {
            case "Product":
                return new OpenApiObject
                {
                    [ "id" ] = new OpenApiInteger(123),
                    [ "description" ] = new OpenApiString("foobar"),
                    [ "price" ] = new OpenApiDouble(14.37)
                };
            default:
                return null;
        }
    }
}