【问题标题】:Transitioning ISchemaFilter examples to Swashbuckle 5.0将 ISchemaFilter 示例转换为 Swashbuckle 5.0
【发布时间】:2020-03-03 21:30:45
【问题描述】:

在 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 Schema 时会正确序列化。

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

基于查看Microsoft.OpenApi 中的代码,我尝试构建我自己的IOpenApiAny 实现,但是任何使用它来生成示例的尝试都在Microsoft.OpenApi.Writers.OpenApiWriterAnyExtensions.WriteObject(IOpenApiWriter writer, OpenApiObject entity) 内部在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 以便遵守适当的序列化规则的正确方法是什么?

【问题讨论】:

    标签: c# openapi swashbuckle asp.net-core-3.0 .net-core-3.0


    【解决方案1】:

    他们在 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;
            }
        }
    }
    

    【讨论】:

    • 这似乎在功能上是正确的,但让我害怕的是,新常态完全忽略了配置的序列化设置(例如属性大小写、枚举和日期处理等),并且本质上断开了与实际模型结构相关联。在我看来似乎是退步了。
    • 该评论是您应该添加到 repo 中的内容,我们无能为力
    • 同意,我并不是要暗示责备,只是在根据您的回复继续调查时评论当前的想法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-13
    • 1970-01-01
    • 2012-07-24
    • 2015-04-15
    • 1970-01-01
    • 2021-09-30
    相关资源
    最近更新 更多