Перевод примеров ISchemaFilter в Swashbuckle 5.0 - PullRequest
6 голосов
/ 04 марта 2020

До 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, но любая попытка использовать его для генерации примера терпит неудачу из 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, чтобы соблюдались соответствующие правила сериализации?

1 Ответ

1 голос
/ 05 марта 2020

У них есть пример на репо:
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;
        }
    }
}
...