如何让Swagger UI显示GET请求中复杂Record的查询参数
问题描述
我在ASP.NET Core的GET接口中,使用自定义绑定的Record SearchProductsRequest 接收查询字符串参数,示例URL如下:/v1/products?ids=1,2,3&name=hombre&page=3&pageItems=4&sortField=name&sort=asc
接口核心代码:
app.MapGet( $"/{ProductCatalogueApi.Version}/products", (SearchProductsRequest request) => ProductApiDelegates.SearchProducts(request));
我已经在SearchProductsRequest中实现了BindAsync方法,能正常将URL参数转换为Record实例,但Swagger UI无法识别该Record的成员,无法生成对应的参数输入框。目前只有把所有参数显式写在MapGet方法参数中,Swagger才能正常展示参数。
SearchProductsRequest完整定义:
public record SearchProductsRequest { public IEnumerable<int>? Ids { get; private set; } public string? Name { get; private set; } public PaginationInfoRequest? PaginationInfo { get; private set; } public SortingInfoRequest? SortingInfo { get; private set; } public SearchProductsRequest( IEnumerable<int>? ids, string? name, PaginationInfoRequest? PaginationInfo, SortingInfoRequest? SortingInfo) { this.Ids = ids; this.Name = name; this.PaginationInfo = PaginationInfo; this.SortingInfo = SortingInfo; } public static ValueTask<SearchProductsRequest?> BindAsync( HttpContext httpContext, ParameterInfo parameter) { var ids = ParseIds(httpContext); var name = httpContext?.Request.Query["name"] ?? string.Empty; PaginationInfoRequest? pagination = null; SortingInfoRequest? sorting = null; if (int.TryParse(httpContext?.Request.Query["page"], out var page) && int.TryParse(httpContext?.Request.Query["pageItems"], out var pageItems)) { pagination = new PaginationInfoRequest(page, pageItems); } var sortField = httpContext?.Request.Query["sortField"].ToString(); if (!string.IsNullOrEmpty(sortField)) { sorting = new SortingInfoRequest( sortField, httpContext?.Request.Query["sort"].ToString() == "asc"); } return ValueTask.FromResult<SearchProductsRequest?>( new SearchProductsRequest( ids, name!, pagination, sorting)); } private static int[]? ParseIds(HttpContext httpContext) { int[]? ids = null; var commaSeparatedIds = httpContext?.Request.Query["ids"].ToString(); if (!string.IsNullOrEmpty(commaSeparatedIds)) { ids = commaSeparatedIds .Split(",") .Select(int.Parse) .ToArray() ?? Array.Empty<int>(); } return ids; } }
解决方案
要让Swagger UI识别自定义绑定的Record成员,需要创建自定义参数过滤器,手动将Record的属性(包括嵌套对象的属性)映射为Swagger的查询参数。
步骤1:创建自定义参数过滤器
创建一个实现IParameterFilter的类,针对SearchProductsRequest的结构,将其成员转换为对应的Swagger查询参数:
using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using Microsoft.AspNetCore.Mvc.ApiExplorer; public class SearchProductsRequestParameterFilter : IParameterFilter { public void Apply(OpenApiParameter parameter, ParameterFilterContext context) { // 仅处理SearchProductsRequest类型的参数 if (context.ParameterInfo.ParameterType != typeof(SearchProductsRequest)) return; // 清空Swagger默认生成的单一request参数 context.ApiDescription.ParameterDescriptions.Clear(); // 逐个添加查询参数 AddQueryParameter(context, "ids", typeof(IEnumerable<int>), "逗号分隔的产品ID列表", false); AddQueryParameter(context, "name", typeof(string), "产品名称关键词", false); AddQueryParameter(context, "page", typeof(int), "页码", false); AddQueryParameter(context, "pageItems", typeof(int), "每页条数", false); AddQueryParameter(context, "sortField", typeof(string), "排序字段", false); AddQueryParameter(context, "sort", typeof(string), "排序方向:asc/desc", false); } private void AddQueryParameter(ParameterFilterContext context, string paramName, Type paramType, string description, bool isRequired) { var paramDesc = new ApiParameterDescription { Name = paramName, Type = paramType, Source = Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Query, IsRequired = isRequired, Description = description }; context.ApiDescription.ParameterDescriptions.Add(paramDesc); } }
步骤2:注册过滤器到Swagger配置
在Program.cs的Swagger配置中,添加这个自定义过滤器:
builder.Services.AddSwaggerGen(c => { // 其他Swagger配置(如文档标题、版本等) c.ParameterFilter<SearchProductsRequestParameterFilter>(); });
可选:通用嵌套对象处理(扩展方案)
如果需要支持任意带自定义绑定的Record类型,可以实现通用过滤器,通过反射自动解析嵌套属性:
using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using Microsoft.AspNetCore.Mvc.ApiExplorer; using System.Reflection; public class GenericRecordParameterFilter : IParameterFilter { public void Apply(OpenApiParameter parameter, ParameterFilterContext context) { var paramType = context.ParameterInfo.ParameterType; // 仅处理带BindAsync静态方法的Record类型 if (!paramType.IsRecord() || !HasBindAsyncMethod(paramType)) return; context.ApiDescription.ParameterDescriptions.Clear(); // 递归解析所有属性(包括嵌套对象) ParseProperties(context, paramType, string.Empty); } private void ParseProperties(ParameterFilterContext context, Type type, string parentPrefix) { foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var propType = prop.PropertyType; var paramKey = string.IsNullOrEmpty(parentPrefix) ? prop.Name : $"{parentPrefix}.{prop.Name}"; // 处理嵌套复杂类型 if (!propType.IsValueType && propType != typeof(string) && !propType.IsEnum) { ParseProperties(context, propType, paramKey); continue; } // 映射为URL中的查询参数名(和BindAsync逻辑对应) var queryParamName = paramKey switch { "PaginationInfo.Page" => "page", "PaginationInfo.PageItems" => "pageItems", "SortingInfo.SortField" => "sortField", "SortingInfo.IsAscending" => "sort", _ => char.ToLowerInvariant(paramKey[0]) + paramKey.Substring(1) // 默认小驼峰转换 }; AddQueryParameter(context, queryParamName, propType, prop.Name, false); } } private void AddQueryParameter(ParameterFilterContext context, string paramName, Type paramType, string description, bool isRequired) { var paramDesc = new ApiParameterDescription { Name = paramName, Type = paramType, Source = Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Query, IsRequired = isRequired, Description = description }; context.ApiDescription.ParameterDescriptions.Add(paramDesc); } private bool HasBindAsyncMethod(Type type) { return type.GetMethod("BindAsync", BindingFlags.Public | BindingFlags.Static) != null; } }
注册通用过滤器:
builder.Services.AddSwaggerGen(c => { c.ParameterFilter<GenericRecordParameterFilter>(); });
效果验证
启动项目后,Swagger UI会显示所有对应的查询参数输入框,和显式声明参数的效果一致,用户可直接在Swagger中输入参数并调用接口。
内容的提问来源于stack exchange,提问作者Daniel Mendonça
相关产品推荐
相关产品推荐

