Clean Architecture+MediatR如何处理SelectListItem不破坏依赖链
我正在开发一款「管理端应用」,用于管理另一款社区驱动的歌词Web应用。注册用户可提交新歌手、歌词信息,该管理端供管理员、审核员完成新提交内容审批等操作。
管理端基于Jason Taylor的Clean Architecture解决方案模板搭建。原模板前端采用Angular构建的SPA应用,我需要使用常规MVC搭配Razor视图的模式,因此移除了Client项目,修改控制器使其返回视图而非JSON数据。
前期开发进展顺利。
我现在需要实现新增歌手功能,包含两类请求动作:初始GET请求用于加载表单,POST请求用于处理表单提交。
处理GET请求时,返回给Razor视图的视图模型需要包含SelectListItem类型属性,用于渲染性别选择下拉框。
ArtistsController.cs
[HttpGet] public async Task<ActionResult> Create() { CreateArtistGenderDto viewModel = await Mediator.Send(new CreateArtistQuery()); return View(viewModel); } [HttpPost] public async Task<ActionResult<int>> Create(CreateArtistCommand command) { return await Mediator.Send(command); }
CreateArtistQuery.cs
public class CreateArtistQuery : IRequest<CreateArtistGenderDto> { } public class CreateArtistQueryHandler : IRequestHandler<CreateArtistQuery, CreateArtistGenderDto> { public async Task<CreateArtistGenderDto> Handle( CreateArtistQuery request, CancellationToken cancellationToken) { CreateArtistGenderDto model = new CreateArtistGenderDto(); return model; } }
CreateArtistGenderDto.cs
namespace bejebeje.admin.Application.Artists.Queries.CreateArtist; public class CreateArtistGenderDto { public Gender SelectedGender { get; set; } public List<SelectListItem> GenderOptions = new List<SelectListItem> { new SelectList(Enum.GetValues(typeof(Gender)) }; } public enum Gender { Female, Male, NotApplicable }
当前遇到的问题是:如果遵循MediatR开发规范,将Query类放在Application项目中,该项目无法识别SelectListItem类型;如果为Application项目引入对应依赖包,就会破坏Clean Architecture原本的依赖规则。

Clean Architecture要求依赖始终向内流动,引入该依赖后会出现向外的依赖引用,违反架构设计原则,最终会导致项目结构混乱。

核心诉求:在不破坏Clean Architecture依赖流向规则的前提下,正确实现下拉选项绑定需求。
核心原则是UI层专属类型绝对不能出现在Application层,SelectListItem是ASP.NET Core MVC专属的UI渲染类型,本身就属于Presentation层的职责范畴,不要让Application层感知它的存在,有两种成熟实现方案可选:
方案1:分层定义DTO,UI层做映射(最推荐,完全符合架构规范)
- 修改Application层的返回DTO,只返回和业务相关的原始数据,完全移除UI专属类型:
// Application层DTO,不依赖任何UI框架 namespace bejebeje.admin.Application.Artists.Queries.CreateArtist; public class CreateArtistGenderDto { public Gender SelectedGender { get; set; } // 只返回业务层面的枚举值列表,不需要关心前端渲染逻辑 public IReadOnlyList<Gender> AvailableGenders { get; set; } = Enum.GetValues<Gender>().ToList(); } public enum Gender { Female, Male, NotApplicable } - 在Web(Presentation)层定义专属视图模型,接收Application层返回的业务DTO后,在Controller中映射为视图需要的结构:
// Web层视图模型,可正常引用SelectListItem namespace bejebeje.admin.Web.ViewModels.Artists; public class CreateArtistViewModel { public Gender SelectedGender { get; set; } public List<SelectListItem> GenderOptions { get; set; } = new(); } - 修改Controller的GET方法,完成映射逻辑:
[HttpGet] public async Task<ActionResult> Create() { var businessDto = await Mediator.Send(new CreateArtistQuery()); var viewModel = new CreateArtistViewModel { SelectedGender = businessDto.SelectedGender, GenderOptions = businessDto.AvailableGenders.Select(g => new SelectListItem { Text = g.ToString(), Value = ((int)g).ToString() }).ToList() }; return View(viewModel); }
这种方案职责完全清晰,Application层只处理业务逻辑,和UI技术栈完全解耦,未来如果换成API返回JSON、Blazor或者其他前端框架,Application层代码不需要做任何修改。
方案2:UI层直接构造固定下拉选项(轻量场景首选)
针对性别枚举这类固定不变的选项,完全没必要通过Mediator传递选项数据,可以把构造下拉选项的逻辑完全放在UI层:
- Application层的DTO只保留和业务提交相关的字段即可:
public class CreateArtistGenderDto { public Gender SelectedGender { get; set; } } - 直接在Razor视图中用内置方法生成下拉选项,不需要额外传递数据:
<select asp-for="SelectedGender" asp-items="Html.GetEnumSelectList<Gender>()" class="form-control"></select>
这种方案代码量最少,GetEnumSelectList是Razor内置的辅助方法,可以直接根据枚举类型生成符合要求的SelectListItem集合,不需要额外写映射逻辑。
不要为了省事给Application层引入
Microsoft.AspNetCore.Mvc.ViewFeatures包(SelectListItem所在的依赖包),一旦打破分层依赖约束,后续会有越来越多UI逻辑泄露到Application层,最终Clean Architecture的分层设计会完全失效。
内容的提问来源于stack exchange,提问作者J86

