ASP.NET Core6兼容旧Web API2 List模型绑定实现方法
问题说明
将遗留ASP.NET Web API 2项目迁移至ASP.NET Core 6时,HTTP GET接口的列表类型查询参数绑定失效:
入参类定义如下:
public class GetUsersArgs { public List<int> UserIds { get; set; } }
旧客户端固定使用api/Users/GetUsers?UserIds[]=1&UserIds[]=5&UserIds[]=10格式发起调用,该格式在Web API 2中可正常绑定为3个元素的列表,但ASP.NET Core 6默认模型绑定规则无法识别带[]后缀的集合参数名,且客户端代码不可修改。
实现方案
ASP.NET Core默认集合绑定仅识别同名多值参数(即?UserIds=1&UserIds=5&UserIds=10格式),要兼容旧版带方括号后缀的参数格式,可选择以下两种实现方式:
方案1:自定义全局模型绑定器(推荐)
一次配置即可让所有接口的List<T>类型查询参数同时兼容带[]后缀和不带后缀的两种格式,对业务代码无侵入。
- 实现自定义列表模型绑定器,逻辑中同时匹配两种格式的参数名:
using Microsoft.AspNetCore.Mvc.ModelBinding; using System; using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; public class BracketSuffixListModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var modelType = bindingContext.ModelType; // 仅处理List<T>类型的绑定 if (!modelType.IsGenericType || modelType.GetGenericTypeDefinition() != typeof(List<>)) return Task.CompletedTask; var elementType = modelType.GetGenericArguments()[0]; var paramName = bindingContext.ModelName; // 优先匹配无后缀参数名,未命中则尝试匹配带[]后缀的参数名 var valueResult = bindingContext.ValueProvider.GetValue(paramName); if (valueResult == ValueProviderResult.None) valueResult = bindingContext.ValueProvider.GetValue($"{paramName}[]"); if (valueResult == ValueProviderResult.None) return Task.CompletedTask; bindingContext.ModelState.SetModelValue(paramName, valueResult); try { var listInstance = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)); foreach (var rawValue in valueResult) { if (string.IsNullOrWhiteSpace(rawValue)) continue; listInstance.Add(Convert.ChangeType(rawValue, elementType)); } bindingContext.Result = ModelBindingResult.Success(listInstance); } catch (Exception ex) { bindingContext.ModelState.TryAddModelError(paramName, $"参数值转换失败:{ex.Message}"); } return Task.CompletedTask; } }
- 实现模型绑定提供器,用于将上述绑定器匹配到List类型的模型:
using Microsoft.AspNetCore.Mvc.ModelBinding; using System; public class BracketSuffixListModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); if (context.Metadata.ModelType.IsGenericType && context.Metadata.ModelType.GetGenericTypeDefinition() == typeof(List<>)) { return new BinderTypeModelBinder(typeof(BracketSuffixListModelBinder)); } return null; } }
- 在
Program.cs中注册绑定提供器,注意要插入到默认集合绑定提供器之前,保证优先执行:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { // 插入到绑定提供器列表首位,优先处理List类型参数 options.ModelBinderProviders.Insert(0, new BracketSuffixListModelBinderProvider()); }); // 其余服务注册、中间件配置按原有逻辑编写 var app = builder.Build(); app.Run();
方案2:单属性指定参数别名
如果仅少量接口需要兼容旧格式,可直接在入参类的对应属性上,通过FromQuery特性显式指定带[]后缀的参数名:
public class GetUsersArgs { [FromQuery(Name = "UserIds[]")] public List<int> UserIds { get; set; } }
该方案实现简单,但每个需要兼容的属性都要手动加特性,且默认无法同时兼容不带[]后缀的新调用格式,适合接口量极少的场景使用。
注意:不建议通过重写默认查询字符串值提供器的方式做全局参数名替换,该方式会影响所有查询参数的匹配逻辑,容易引发非集合类型的绑定异常。
内容的提问来源于stack exchange,提问作者YuriyP
相关产品推荐
相关产品推荐

