.NET Core WebApi中HttpGet接口如何正确绑定复杂查询参数到FromQuery模型
.NET Core GET接口复杂查询字符串绑定规范实现方案
默认[FromQuery]模型绑定无法直接解析你传递的URL编码JSON格式Filters参数,此前你实现的「转字典→转JSON→反序列化」自定义绑定器侵入性强、冗余度高,且容易跳过框架内置的模型校验、值处理管道,以下两种方案均基于.NET原生扩展点实现,符合框架设计规范:
方案1:TypeConverter实现(优先推荐,改动最小)
该方案仅需为Filters属性添加类型转换逻辑,完全复用默认模型绑定管道,性能最优、无额外副作用。
- 第一步:实现通用JSON字符串类型转换器
using System.ComponentModel; using System.Globalization; using System.Text.Json; public class JsonFilterTypeConverter<T> : TypeConverter where T : IAdvancedFilter, new() { public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); } public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is not string jsonStr) return base.ConvertFrom(context, culture, value); if (string.IsNullOrWhiteSpace(jsonStr)) return new T(); // URL解码后反序列化,大小写不敏感适配前端传参习惯 jsonStr = Uri.UnescapeDataString(jsonStr); return JsonSerializer.Deserialize<T>(jsonStr, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } }
- 第二步:为基类的
Filters属性添加转换器标记
修改PaginationRequest<T>基类代码:
public class PaginationRequest<T> where T : IAdvancedFilter?, new() { public int? Page { get; set; } public int? RowsPerPage { get; set; } public string? SortBy { get; set; } public bool Descending { set; get; } public string? Search { set; get; } [TypeConverter(typeof(JsonFilterTypeConverter<T>))] public T? Filters { get; set; } = new (); }
完成以上修改后,原有Action代码不需要做任何调整,默认[FromQuery]绑定器会自动调用转换器完成Filters属性的解析,内置模型校验、空值处理逻辑全部正常生效。
方案2:全局值提供者实现(适合多接口统一规范场景)
如果项目中有大量接口需要从QueryString接收JSON格式复杂参数,可通过自定义值提供者在参数解析阶段统一处理,不需要修改任何模型类代码。
- 实现JSON查询值提供者及工厂
public class JsonQueryValueProvider : QueryStringValueProvider { private readonly IQueryCollection _queryCollection; public JsonQueryValueProvider(BindingSource bindingSource, IQueryCollection query, CultureInfo culture) : base(bindingSource, query, culture) { _queryCollection = query; } public override ValueProviderResult GetValue(string key) { var baseResult = base.GetValue(key); if (baseResult != ValueProviderResult.None) return baseResult; if (!_queryCollection.TryGetValue(key, out var val)) return baseResult; var rawVal = Uri.UnescapeDataString(val.ToString()).Trim(); if (!rawVal.StartsWith("{")) return baseResult; // 展平JSON对象为属性路径键值对,适配默认模型绑定逻辑 var resultDict = new Dictionary<string, string>(); using var doc = JsonDocument.Parse(rawVal); FlattenJson(doc.RootElement, key, resultDict); return new ValueProviderResult(new StringValues(resultDict.Values.ToArray())); } private void FlattenJson(JsonElement node, string prefix, Dictionary<string, string> output) { if (node.ValueKind == JsonValueKind.Object) { foreach (var prop in node.EnumerateObject()) { FlattenJson(prop.Value, $"{prefix}.{prop.Name}", output); } return; } output[prefix] = node.ToString(); } } public class JsonQueryValueProviderFactory : IValueProviderFactory { public Task CreateValueProviderAsync(ValueProviderFactoryContext context) { if (context.ActionContext.HttpContext.Request.Query == null) return Task.CompletedTask; context.ValueProviders.Insert(0, new JsonQueryValueProvider( BindingSource.Query, context.ActionContext.HttpContext.Request.Query, CultureInfo.InvariantCulture)); return Task.CompletedTask; } }
- 在Program.cs中注册值提供者
builder.Services.AddControllers(options => { options.ValueProviderFactories.Insert(0, new JsonQueryValueProviderFactory()); });
该方案会自动识别所有QueryString中传递的JSON格式参数,展平为默认绑定器可识别的层级结构,支持任意层级嵌套的复杂对象绑定。
注意事项
- 你给出的示例查询字符串中
Filters的JSON存在语法错误:原始值{"ProductId":14125F3D-3662-45DC-895C-691911A4767B""}存在多余双引号、Guid类型值未加字符串引号的问题,正确的未编码JSON应为{"ProductId":"14125F3D-3662-45DC-895C-691911A4767B"},否则反序列化会失败。 - 坚持使用GET请求实现分页查询是符合RESTful语义的合理选择,不需要为了绑定参数强制改用POST。
内容的提问来源于stack exchange,提问作者user19134022
相关产品推荐
相关产品推荐

