如何在ASP.NET Core中创建支持多个可选字符串参数的路由
解决方案
ASP.NET Core 原生的路由模板不支持任意顺序、任意数量的可选路径段绑定,因为你无法提前预知参数的出现顺序和数量,所以无法将参数位置写死在路由模板中,需要用「捕获全部剩余路径+自定义解析」的逻辑实现需求,以下是两种落地方式:
方案1:手动解析(快速实现)
适合需求简单、不需要复用解析逻辑的场景,直接修改路由模板后在Action内完成参数解析:
- 修改路由模板,用
*filters捕获所有必填段后的路径内容:
// *filters 是 catch-all 路由参数,会把id之后所有的路径内容(含斜杠)全部作为值传入 [Route("/homes-for-sale-in-{city}-{id:int}/{*filters}", Name = "HomesForSaleByCity")] public async Task<IActionResult> Display(int id, string city, string filters) { var viewModel = new SearchViewModel { Id = id, City = city }; // 解析可选参数 if (!string.IsNullOrWhiteSpace(filters)) { // 拆分所有参数段 var segments = filters.Split('/', StringSplitOptions.RemoveEmptyEntries); foreach (var seg in segments) { // 取最后一个下划线作为分隔符,兼容参数值中包含下划线的场景 var splitIndex = seg.LastIndexOf('_'); if (splitIndex < 0 || splitIndex == seg.Length - 1) continue; var value = seg.Substring(0, splitIndex); var paramName = seg.Substring(splitIndex + 1).ToLower(); // 匹配赋值 switch (paramName) { case "price": viewModel.Price = value; break; case "type": viewModel.Type = value; break; case "beds": viewModel.Beds = value; break; case "baths": viewModel.Baths = value; break; case "page": if (int.TryParse(value, out var page)) viewModel.Page = page; break; } } } // 此处可加你自己的ViewModel校验逻辑 return View(viewModel); }
方案2:自定义模型绑定(复用性更高)
适合多场景需要用到该参数解析逻辑的场景,把解析逻辑封装到绑定器中,Action代码可以保持简洁:
- 实现自定义模型绑定器:
public class SearchViewModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var model = new SearchViewModel(); // 读取路由基础参数 if (int.TryParse(bindingContext.ValueProvider.GetValue("id").FirstValue, out var id)) model.Id = id; model.City = bindingContext.ValueProvider.GetValue("city").FirstValue; var filterStr = bindingContext.ValueProvider.GetValue("filters").FirstValue; if (string.IsNullOrWhiteSpace(filterStr)) { bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } // 解析可选参数 var segments = filterStr.Split('/', StringSplitOptions.RemoveEmptyEntries); foreach (var seg in segments) { var splitIndex = seg.LastIndexOf('_'); if (splitIndex < 0 || splitIndex == seg.Length - 1) continue; var value = seg.Substring(0, splitIndex); var paramName = seg.Substring(splitIndex + 1); // 反射匹配ViewModel属性 var prop = typeof(SearchViewModel).GetProperties() .FirstOrDefault(p => p.Name.Equals(paramName, StringComparison.OrdinalIgnoreCase)); if (prop == null || !prop.CanWrite) continue; // 类型转换 if (prop.PropertyType == typeof(string)) prop.SetValue(model, value); else if (prop.PropertyType == typeof(int) && int.TryParse(value, out var intVal)) prop.SetValue(model, intVal); else if (prop.PropertyType == typeof(int?)) prop.SetValue(model, int.TryParse(value, out var intVal) ? intVal : null); } bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } }
- 给ViewModel绑定绑定器:
[ModelBinder(BinderType = typeof(SearchViewModelBinder))] public class SearchViewModel { [Required] public int? Id { get; set; } public string City { get; set; } public string Price { get; set; } public string Type { get; set; } public string Beds { get; set; } public string Baths { get; set; } public int Page { get; set; } }
- Action保持原有写法即可:
[Route("/homes-for-sale-in-{city}-{id:int}/{*filters}", Name = "HomesForSaleByCity")] public async Task<IActionResult> Display(SearchViewModel viewModel) { // viewModel已自动完成所有参数绑定 return View(); }
注意事项
- 用
LastIndexOf('_')拆分参数而不是Split('_'),可以避免参数值中包含下划线的场景下解析错误 - catch-all参数
*filters会自动忽略路径末尾多余的斜杠,兼容性更好 - 可以在解析逻辑中增加参数白名单校验,避免无关参数被错误赋值到ViewModel中
内容的提问来源于stack exchange,提问作者Jay
相关产品推荐
相关产品推荐

