Swagger(C# 的 Swashbuckle)将 Mongo ObjectId 显示为多个字段而不是单个字符串

Swagger (Swashbuckle for C#) shows Mongo ObjectId as several fields instead of single string

我有带 ObjectId 参数的控制器方法:

[ProducesResponseType(200, Type = typeof(Test))]
[HttpGet]
[Route("{id}")]
public IActionResult Get(ObjectId id)
{...

为此 API 方法 swagger 生成了一个包含复杂 ObjectId 模型和字符串 Id 而不是单个字符串参数的表单: 如何删除多余的字段并仅保留字符串 ID?

可以过滤 swagger 表单生成器的输出字段:

public class SwaggerOperationFilter : IOperationFilter
{
    private readonly IEnumerable<string> objectIdIgnoreParameters = new[]
    {
        nameof(ObjectId.Timestamp),
        nameof(ObjectId.Machine),
        nameof(ObjectId.Pid),
        nameof(ObjectId.Increment),
        nameof(ObjectId.CreationTime)
    };

    public void Apply(Operation operation, OperationFilterContext context)
    {
        operation.Parameters = operation.Parameters.Where(x =>
            x.In != "query" || objectIdIgnoreParameters.Contains(x.Name) == false
        ).ToList();
    }
}

并在 Startup.cs 中使用此过滤器:

public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddSwaggerGen(options =>
        {
            ...
            options.OperationFilter<SwaggerOperationFilter>();
        });
...

结果,我们只有 Id 字段:

找出 from another 也解决了这个问题:

services.AddMvc(options =>
{                    
    ...
    options.ModelMetadataDetailsProviders.Add(
        new BindingSourceMetadataProvider(typeof(ObjectId), BindingSource.Special));
});

我有同样的问题,但稍微大一点。在我的 API 方法中,我使用 ObjectId:

  • 作为路由参数public IActionResult Get(ObjectId id),
  • 在类内作为查询参数public IActionResult Get([FromQuery] ClassWithObjectId filter),
  • 在 POST 个机构中 public IActionResult Post([FromBody] ClassWithObjectId form)
  • 在响应对象中

此外,我还有 <summary> 标签和 ObjectId 属性,我想在夸张的描述中展示它。 所以很难强迫 Swashbuckle 尊重所有这些案例,但我想我做到了!

我们需要两个过滤器:

  • ObjectIdOperationFilter 强制 Swashbuckle 在属性(路由、查询)中尊重 ObjectId
  • ObjectIdSchemaFilter 强制 Swashbuckle 在正文中尊重 ObjectId(在请求和响应中)

关于 swagger 设置:

//piece of code to add all XML Documentation files. Ignore if you don't need them
var swaggerFiles = new string[] { "SwaggerAPI.xml", "SwaggerApplicationAPI.xml" }
  .Select(fileName => Path.Combine(System.AppContext.BaseDirectory, fileName))
  .Where(filePath => File.Exists(filePath));

foreach (var filePath in swaggerFiles)
  options.IncludeXmlComments(filePath);

//we have to pass swaggerFiles here to add description to ObjectId props
//don't pass them if you won't
options.OperationFilter<ObjectIdOperationFilter>(swaggerFiles);
options.SchemaFilter<ObjectIdSchemaFilter>();

ObjectIdOperationFilter.cs:
(所有使用 XML 的方法我都取自 Swashbuckle.AspNetCore repository

public class ObjectIdOperationFilter : IOperationFilter
{
    //prop names we want to ignore
    private readonly IEnumerable<string> objectIdIgnoreParameters = new[]
    {
        "Timestamp",
        "Machine",
        "Pid",
        "Increment",
        "CreationTime"
    };

    private readonly IEnumerable<XPathNavigator> xmlNavigators;

    public ObjectIdOperationFilter(IEnumerable<string> filePaths)
    {
        xmlNavigators = filePaths != null
            ? filePaths.Select(x => new XPathDocument(x).CreateNavigator())
            : Array.Empty<XPathNavigator>();
    }

    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        //for very parameter in operation check if any fields we want to ignore
        //delete them and add ObjectId parameter instead
        foreach (var p in operation.Parameters.ToList())
            if (objectIdIgnoreParameters.Any(x => p.Name.EndsWith(x)))
            {
                var parameterIndex = operation.Parameters.IndexOf(p);
                operation.Parameters.Remove(p);
                var dotIndex = p.Name.LastIndexOf(".");
                if (dotIndex > -1)
                {
                    var idName = p.Name.Substring(0, dotIndex);
                    if (!operation.Parameters.Any(x => x.Name == idName))
                    {
                        operation.Parameters.Insert(parameterIndex, new OpenApiParameter()
                        {
                            Name = idName,
                            Schema = new OpenApiSchema()
                            {
                                Type = "string",
                                Format = "24-digit hex string"
                            },
                            Description = GetFieldDescription(idName, context),
                            Example = new OpenApiString(ObjectId.Empty.ToString()),
                            In = p.In,
                        });
                    }
                }
            }
    }

    //get description from XML
    private string GetFieldDescription(string idName, OperationFilterContext context)
    {
        var name = char.ToUpperInvariant(idName[0]) + idName.Substring(1);
        var classProp = context.MethodInfo.GetParameters().FirstOrDefault()?.ParameterType?.GetProperties().FirstOrDefault(x => x.Name == name);
        var typeAttr = classProp != null
            ? (DescriptionAttribute)classProp.GetCustomAttribute<DescriptionAttribute>()
            : null;
        if (typeAttr != null)
            return typeAttr?.Description;

        if (classProp != null)
            foreach (var xmlNavigator in xmlNavigators)
            {
                var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(classProp);
                var propertySummaryNode = xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']/summary");
                if (propertySummaryNode != null)
                    return XmlCommentsTextHelper.Humanize(propertySummaryNode.InnerXml);
            }

        return null;
    }
}

ObjectIdSchemaFilter.cs:

public class ObjectIdSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (context.Type == typeof(ObjectId))
        {
            schema.Type = "string";
            schema.Format = "24-digit hex string";
            schema.Example = new OpenApiString(ObjectId.Empty.ToString());
        }
    }
}

而且有效!

Repository with all this filters here