Swashbuckle 没有为数组\列表属性正确生成示例 xml

问题描述 投票:0回答:5

当模型具有列表属性时,swashbuckle\swagger-ui(5.6 - 使用 swagger-ui)似乎无法正确生成示例 XML。

看到这个问题:

1 - 创建一个空的 webapi 项目(我使用的是 asp.net)

2 - 添加几个示例模型(我用 Customer + Order 进行测试)

public class Customer
{
    public string AccountNumber { get; set; }
    [XmlArray("Orders"),XmlArrayItem("Order")]
    public List<Order> Orders { get;set; }
}

public class Order
{
    public string OrderNumber { get;set; }
}

3 - 使用 FromBody 创建控制器以绑定到模型

public class CustomerController : ApiController
{
    public void Post([FromBody]Customer customer)
    {
        customer.ToString();
    }
}

4 - 更改 web api 配置以允许简单的 XML

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Formatters.XmlFormatter.UseXmlSerializer = true;  //ADD THIS
    }
}

5 - 运行站点并使用 /swagger ui 将参数内容类型更改为 xml 并选择示例模型。你会发现例子如下。

<?xml version="1.0"?>
<Customer>
  <AccountNumber>string</AccountNumber>
  <Orders>
    <OrderNumber>string</OrderNumber>
  </Orders>
</Customer>

6 - 在控制器中的 customer.ToString() 行上使用断点提交此代码,您会发现 Orders 集合为空

7 - 将swagger-ui中的XML修改为如下内容并提交:

<?xml version="1.0"?>
<Customer>
  <AccountNumber>string</AccountNumber>
  <Orders>
    <Order><OrderNumber>string</OrderNumber></Order>
  </Orders>
</Customer>

8 -

Customer.Orders
集合现在已正确填充。

在 Swashbuckle 中修复或解决此问题的最佳方法是什么?

(围绕这个问题进行了一些讨论,以及它是否是 swagger-ui 或 Swashbuckle 中的错误,但我特别感兴趣的是使用 Swashbuckle 解决它)

asp.net-web-api swashbuckle
5个回答
1
投票

我找到了以下作品:

1 - 添加 ISchemaFilter 的实现

internal class ApplySchemaVendorExtensions : ISchemaFilter
{
    public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
    {
        // Fix issues with xml array examples not generating correctly
        if (!type.IsValueType)
        {
            schema.xml = new Xml { name = type.Name };
            if(schema.properties != null)
            {
                foreach (var property in schema.properties)
                {
                    //Array property, which wraps its elements
                    if (property.Value.type == "array")
                    {
                        property.Value.xml = new Xml
                        {
                            name = $"{property.Key}",
                            wrapped = true
                        };
                    }
                }
            }
        }
    }
}

2 - 将这一行注释到 SwaggerConfig.cs

c.SchemaFilter<ApplySchemaVendorExtensions>();

重复问题中的测试,示例 XML 现在可以直接运行。一如既往,我很好奇是否有更好的解决方案......

编辑:实际上这在我遇到这个问题的原始项目中很奇怪,但在这个问题的小型复制项目中它的行为略有不同!当我找到原因时,我会编辑这个答案......


1
投票

感谢@mutex,但是我发现我需要再对它做一个调整:

internal class SwaggerFixArraysInXmlFilter : Swashbuckle.Swagger.ISchemaFilter
{
    // this fixes a Swagger bug that wasn't generating correct XML elements around List<> or array[] types
    public void Apply(Swashbuckle.Swagger.Schema schema, Swashbuckle.Swagger.SchemaRegistry schemaRegistry, System.Type type)
    {
        // Fix issues with xml array examples not generating correctly
        if (!type.IsValueType)
        {
            schema.xml = new Swashbuckle.Swagger.Xml { name = type.Name };
            if (schema.properties != null)
            {
                foreach (var property in schema.properties)
                {
                    //Array property, which wraps its elements
                    if (property.Value.type == "array")
                    {
                        property.Value.xml = new Swashbuckle.Swagger.Xml
                        {
                            name = $"{property.Key}",
                            wrapped = true
                        };
                        property.Value.items.xml = new Swashbuckle.Swagger.Xml
                        {
                            name = $"{property.Value.items.type}",
                            wrapped = true
                        };
                    }
                }
            }
        }
    }
}

1
投票

感谢@Abacus,但我发现我需要再次调整它。 (字符串不是 ValueType,因此它正在将任何字符串值重命名为 string...可能与 .NET Core 3.1 有关)

internal class SwaggerFixArraysInXmlFilter : Swashbuckle.Swagger.ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        Type type = context.Type;

        // Fix issues with xml array examples not generating correctly
        if (!type.IsValueType && type.Name != "String")
        {
            schema.Xml = new OpenApiXml { Name = type.Name };
            if (schema.Properties != null)
            {
                foreach (var property in schema.Properties)
                {
                    //Array property, which wraps its elements
                    if (property.Value.Type == "array")
                    {
                        property.Value.Xml = new OpenApiXml
                        {
                            Name = $"{property.Key}",
                            Wrapped = true
                        };
                        property.Value.Items.Xml = new OpenApiXml
                        {
                            Name = $"{property.Value.Items.Type}",
                            Wrapped = true
                        };
                    }
                }
            }
        }
    }

0
投票

如果您使用 .Net Core 2.2 和 Swagger v5,您将需要以下代码集

internal class SwaggerFixArraysInXmlFilter : ISchemaFilter
    {
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            Type type = context.Type;

            // Fix issues with xml array examples not generating correctly
            if (!type.IsValueType)
            {
                schema.Xml = new OpenApiXml { Name = type.Name };
                if (schema.Properties != null)
                {
                    foreach (var property in schema.Properties)
                    {
                        //Array property, which wraps its elements
                        if (property.Value.Type == "array")
                        {
                            property.Value.Xml = new OpenApiXml
                            {
                                Name = $"{property.Key}",
                                Wrapped = true
                            };
                            property.Value.Items.Xml = new OpenApiXml
                            {
                                Name = $"{property.Value.Items.Type}",
                                Wrapped = true
                            };
                        }
                    }
                }
            }
        }
    }

0
投票

我正在使用以下

ISchemaFilter
(.NET 6.0,Swashbuckle.AspNetCore 6.5.0):

public class XmlSchemaFilter : ISchemaFilter
{
    private static string TakeChars(string src, int charCount)
    {
        if (charCount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(charCount), $@"{nameof(charCount)} must be greater than 0");
        }

        return src.Length <= charCount
            ? src
            : src[..charCount];
    }

    private static string SafeSubstring(string src, int startIndex)
    {
        if (startIndex < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(startIndex), $@"{nameof(startIndex)} must be greater than or equal to 0");
        }

        return src.Length - 1 < startIndex
            ? string.Empty
            : src[startIndex..];
    }

    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (schema.Properties == null)
            return;

        var type = context.Type;

        if (type.GetCustomAttribute<ApplyXmlSchemaFilterAttribute>() == null)
            return;

        var typeXmlRootAttribute = type.GetCustomAttribute<XmlRootAttribute>();
        if (typeXmlRootAttribute != null)
        {
            schema.Xml ??= new OpenApiXml();
            schema.Xml.Name = typeXmlRootAttribute.ElementName;
        }

        var typeProperties = type.GetProperties();

        var excludedProperties = typeProperties
            .Where(t =>
                t.GetCustomAttribute<XmlIgnoreAttribute>()
                != null)
            .ToList();

        // remove excluded properties ([XmlIgnore])
        foreach (var schemaProperty in schema.Properties.ToArray())
        {
            if (excludedProperties.Any(ep =>
                    string.Equals(ep.Name, schemaProperty.Key, StringComparison.InvariantCultureIgnoreCase)))
            {
                schema.Properties.Remove(schemaProperty.Key);
            }
        }

        var restTypeProperties = typeProperties.Except(excludedProperties).ToList();

        // rename properties according to [XmlAttribute], [XmlElement], [XmlRoot], etc.
        foreach (var typeProperty in restTypeProperties)
        {
            var camelCaseKey = TakeChars(typeProperty.Name, 1).ToLowerInvariant() + SafeSubstring(typeProperty.Name, 1);
            var normalKey = typeProperty.Name;

            var (propSchemaKey, propSchema) = typeProperty.Name switch
            {
                not null when schema.Properties.TryGetValue(camelCaseKey, out var openApiSchema) => (camelCaseKey, openApiSchema),
                not null when schema.Properties.TryGetValue(normalKey, out var openApiSchema) => (normalKey, openApiSchema),
                _ => (null, null),
            };

            if (propSchemaKey == null || propSchema == null)
            {
                continue;
            }

            propSchema.Xml ??= new OpenApiXml();

            if (typeProperty.GetCustomAttribute<XmlTextAttribute>() is not null)
            {
                schema.Properties.Remove(propSchemaKey);
                schema.Type = "string";
                schema.Format = null;
            }
            else if (typeProperty.GetCustomAttribute<XmlElementAttribute>() is { } xmlElementAttribute)
            {
                propSchema.Xml.Name = xmlElementAttribute.ElementName;
            }
            else if (typeProperty.GetCustomAttribute<XmlAttributeAttribute>() is { } xmlAttributeAttribute)
            {
                propSchema.Xml.Name = xmlAttributeAttribute.AttributeName;
                propSchema.Xml.Attribute = true;
            }
            else if (typeProperty.GetCustomAttribute<XmlArrayAttribute>() is { } xmlArrayAttribute)
            {
                propSchema.Xml.Name = xmlArrayAttribute.ElementName;
            }
            else
            {
                propSchema.Xml.Name = typeProperty.Name;
            }

            if (typeProperty.GetCustomAttribute<XmlArrayItemAttribute>() is { } xmlArrayItemAttribute
                && type.Name != "String"
                && propSchema.Type == "array")
            {
                // array property, which wraps its elements
                propSchema.Xml.Wrapped = true;

                if (typeProperty.PropertyType.IsGenericType)
                {
                    propSchema.Items = context.SchemaGenerator.GenerateSchema(
                        typeProperty.PropertyType.GetGenericArguments()[0],
                        context.SchemaRepository,
                        typeProperty);
                }

                propSchema.Items.Xml ??= new OpenApiXml();
                propSchema.Items.Xml.Name = xmlArrayItemAttribute.ElementName;
                propSchema.Items.Xml.Wrapped = true;
            }
        }
    }
}

模型应该有

ApplyXmlSchemaFilterAttribute

[System.AttributeUsage(System.AttributeTargets.Class)]
public class ApplyXmlSchemaFilterAttribute : System.Attribute
{
}
© www.soinside.com 2019 - 2024. All rights reserved.