如何解决ASP.NET Core中CQRS RESTful API的参数重复问题
CQRS+MediatR 中API路由参数与查询对象重复的解决方案
你遇到的核心问题是:路由中的PropertyReference与MediatR查询对象的参数重复,导致调用者需重复传递参数,还得额外校验一致性。你的初始思路是可行的,但可以通过几种优化方案降低维护成本,以下是具体实现:
方案1:直接在控制器构造查询对象(最简洁)
无需额外创建GetItemPropertyOptionsRequest类,直接从路由和查询参数中取值构造GetItemPropertyOptionsQuery,避免重复代码:
[HttpGet] [Route("ItemProperties/{propertyReference}/Options")] public async Task<ActionResult<PaginatedList<ItemPropertyOptionDto>>> GetAsync( string propertyReference, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) { var query = new GetItemPropertyOptionsQuery(propertyReference, pageNumber, pageSize); return Ok(await Mediator.Send(query)); }
优点:
- 无需维护额外的Request类,代码结构更紧凑
- 修改查询对象参数时,只需同步修改控制器方法参数,维护成本低
缺点:
- 若查询参数过多,控制器方法参数列表会变长,但分页类参数数量较少时影响可以忽略
方案2:自定义模型绑定器(优雅绑定路由+查询参数)
通过自定义模型绑定逻辑,让ASP.NET Core自动将路由参数和查询参数绑定到MediatR查询对象上,无需手动构造:
步骤1:实现模型绑定器
public class GetItemPropertyOptionsQueryBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); // 从路由中获取PropertyReference var propertyReference = bindingContext.HttpContext.GetRouteValue("propertyReference")?.ToString(); if (string.IsNullOrEmpty(propertyReference)) { bindingContext.ModelState.TryAddModelError(nameof(GetItemPropertyOptionsQuery.PropertyReference), "PropertyReference为必填项"); return Task.CompletedTask; } // 从查询参数中解析分页参数 var pageNumber = int.TryParse(bindingContext.ValueProvider.GetValue("PageNumber").FirstValue, out var pn) ? pn : 1; var pageSize = int.TryParse(bindingContext.ValueProvider.GetValue("PageSize").FirstValue, out var ps) ? ps : 10; // 构造查询对象并返回绑定结果 var query = new GetItemPropertyOptionsQuery(propertyReference, pageNumber, pageSize); bindingContext.Result = ModelBindingResult.Success(query); return Task.CompletedTask; } }
步骤2:给查询对象添加绑定器特性
[ModelBinder(BinderType = typeof(GetItemPropertyOptionsQueryBinder))] public record GetItemPropertyOptionsQuery(string PropertyReference, int PageNumber = 1, int PageSize= 10) : IRequest<PaginatedList<ItemPropertyOptionDto>>;
步骤3:简化控制器代码
[HttpGet] [Route("ItemProperties/{propertyReference}/Options")] public async Task<ActionResult<PaginatedList<ItemPropertyOptionDto>>> GetAsync([FromQuery] GetItemPropertyOptionsQuery query) { return Ok(await Mediator.Send(query)); }
优点:
- 控制器代码极度简洁,调用者只需传递路由参数,查询参数按需传递
- 查询对象参数修改时,只需同步更新绑定器逻辑(参数较少时基本无需调整)
缺点:
- 需要编写额外的绑定器代码,适合参数较多或多个查询对象需要类似绑定逻辑的场景,否则有过度设计嫌疑
方案3:复用参数结构(优化你的初始思路)
如果坚持使用独立的Request类,可以通过基类继承减少重复代码:
// 基类:封装分页通用参数 public record PagedRequestBase(int PageNumber = 1, int PageSize= 10); // MediatR查询对象:添加路由专属参数 public record GetItemPropertyOptionsQuery(string PropertyReference, int PageNumber = 1, int PageSize= 10) : PagedRequestBase(PageNumber, PageSize), IRequest<PaginatedList<ItemPropertyOptionDto>>; // API请求对象:复用基类参数 public record GetItemPropertyOptionsRequest : PagedRequestBase;
控制器代码保持你初始的逻辑:
[HttpGet] [Route("ItemProperties/{propertyReference}/Options")] public async Task<ActionResult<PaginatedList<ItemPropertyOptionDto>>> GetAsync([FromQuery]GetItemPropertyOptionsRequest request, string propertyReference) { var query = new GetItemPropertyOptionsQuery(propertyReference, request.PageNumber, request.PageSize); return Ok(await Mediator.Send(query)); }
优点:
- 修改分页参数时只需更新基类,无需同步修改Request和Query
- 降低参数重复定义的维护成本
缺点:
- 仍需维护额外的类结构,适合有大量分页查询的场景
总结
你的初始思路是可行的,但方案1在参数较少时最简洁高效;如果追求更优雅的参数绑定体验,方案2的自定义模型绑定器是更好的选择;方案3适合需要复用通用参数结构的场景。
内容的提问来源于stack exchange,提问作者Pinx0
相关产品推荐
相关产品推荐

