You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ASP.NET Core中创建支持多个可选字符串参数的路由

解决方案

ASP.NET Core 原生的路由模板不支持任意顺序、任意数量的可选路径段绑定,因为你无法提前预知参数的出现顺序和数量,所以无法将参数位置写死在路由模板中,需要用「捕获全部剩余路径+自定义解析」的逻辑实现需求,以下是两种落地方式:


方案1:手动解析(快速实现)

适合需求简单、不需要复用解析逻辑的场景,直接修改路由模板后在Action内完成参数解析:

  1. 修改路由模板,用*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代码可以保持简洁:

  1. 实现自定义模型绑定器:
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;
    }
}
  1. 给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; }
}
  1. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.18 06:15:07