ASP.NET API:如何复用模型并动态设置Payload必填属性
解决方案
问题1:复用同一模型动态设置Required属性
方法1:使用FluentValidation(推荐)
FluentValidation支持为同一个实体类配置多套验证规则,完美适配Add/Update的不同场景,无需修改实体类的特性,也能避免大量代码重复。
步骤:
- 安装NuGet包:
FluentValidation.AspNetCore - 创建验证器,分别定义Add/Update场景的规则:
public class ToDOValidator : AbstractValidator<ToDO> { private readonly IHttpContextAccessor _httpContextAccessor; public ToDOValidator(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; // 通用规则:Update时Id必须大于0 RuleFor(x => x.Id).GreaterThan(0).When(IsUpdateOperation); } // 配置Add场景的必填规则 public void ConfigureForAdd() { RuleFor(x => x.Name).NotEmpty().WithMessage("名称不能为空"); RuleFor(x => x.Description).NotEmpty().WithMessage("描述不能为空"); } // 配置Update场景的可选规则:仅当属性有值时验证非空 public void ConfigureForUpdate() { RuleFor(x => x.Name).NotEmpty().When(x => !string.IsNullOrEmpty(x.Name)); RuleFor(x => x.Description).NotEmpty().When(x => !string.IsNullOrEmpty(x.Description)); } // 判断当前是否为Update操作 private bool IsUpdateOperation() { var actionName = _httpContextAccessor.HttpContext?.Request.RouteValues["action"]?.ToString(); return actionName == "Update"; } }
- 在Program.cs中注册服务,并在控制器中调用对应场景的验证:
// Program.cs builder.Services.AddHttpContextAccessor(); builder.Services.AddFluentValidationAutoValidation(); builder.Services.AddFluentValidationClientsideAdapters(); builder.Services.AddScoped<ToDOValidator>(); // 控制器代码 [HttpPost] public IActionResult Add([FromBody] ToDO todo, [FromServices] ToDOValidator validator) { validator.ConfigureForAdd(); var result = validator.Validate(todo); if (!result.IsValid) { return BadRequest(result.Errors); } // 执行Add业务逻辑 return Ok(); } [HttpPost] public IActionResult Update([FromBody] ToDO todo, [FromServices] ToDOValidator validator) { validator.ConfigureForUpdate(); var result = validator.Validate(todo); if (!result.IsValid) { return BadRequest(result.Errors); } // 执行Update业务逻辑 return Ok(); }
方法2:实现IValidatableObject接口
在实体类中直接实现IValidatableObject接口,根据当前请求的动作名称动态调整验证规则:
public class ToDO : IValidatableObject { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { var httpContext = validationContext.GetService<IHttpContextAccessor>().HttpContext; var actionName = httpContext?.Request.RouteValues["action"]?.ToString(); if (actionName == "Add") { if (string.IsNullOrEmpty(Name)) yield return new ValidationResult("名称不能为空", new[] { nameof(Name) }); if (string.IsNullOrEmpty(Description)) yield return new ValidationResult("描述不能为空", new[] { nameof(Description) }); } else if (actionName == "Update") { if (!string.IsNullOrEmpty(Name) && string.IsNullOrWhiteSpace(Name)) yield return new ValidationResult("名称不能为空白", new[] { nameof(Name) }); if (!string.IsNullOrEmpty(Description) && string.IsNullOrWhiteSpace(Description)) yield return new ValidationResult("描述不能为空白", new[] { nameof(Description) }); } } }
注意:需要在Program.cs中注册IHttpContextAccessor:
builder.Services.AddHttpContextAccessor();
问题2:Update时仅发送修改的属性是否更优?如何实现?
是否更优?
是的,部分更新的方式有以下明显优势:
- 减少网络传输的数据量,尤其当实体包含20+属性时效果显著;
- 符合RESTful规范中
PATCH方法的语义(用于部分资源更新); - 避免覆盖未修改的属性,降低并发更新时的数据冲突风险。
实现方法:使用JsonPatchDocument(推荐)
ASP.NET Core原生支持JsonPatchDocument,可以接收部分更新请求并自动应用到实体上。
步骤:
- 安装NuGet包:
Microsoft.AspNetCore.JsonPatch - 修改Update接口为
HttpPatch方法,使用JsonPatchDocument<ToDO>作为参数:
[HttpPatch("{id}")] public IActionResult Update(int id, [FromBody] JsonPatchDocument<ToDO> patchDoc) { // 从数据库获取原始实体 var existingTodo = _todoRepository.GetById(id); if (existingTodo == null) { return NotFound(); } // 将补丁应用到原始实体 patchDoc.ApplyTo(existingTodo, ModelState); if (!ModelState.IsValid) { return BadRequest(ModelState); } // 结合问题1的验证逻辑,验证更新后的实体 var validator = new ToDOValidator(_httpContextAccessor); validator.ConfigureForUpdate(); var validationResult = validator.Validate(existingTodo); if (!validationResult.IsValid) { return BadRequest(validationResult.Errors); } // 保存更新到数据库 _todoRepository.Update(existingTodo); return Ok(existingTodo); }
- 请求示例(JSON格式):
[ { "op": "replace", "path": "/name", "value": "更新后的待办名称" }, { "op": "replace", "path": "/description", "value": "更新后的待办描述" } ]
其中op支持replace(替换属性值)、add(添加属性)、remove(移除属性)等操作,path为属性的JSON路径,value为新值。
自定义部分更新(备选)
如果不想使用JsonPatch,可以自定义UpdateDTO,结合AutoMapper减少代码重复:
// 仅包含允许更新的属性 public class ToDOUpdateDTO { public string Name { get; set; } public string Description { get; set; } } // 控制器代码 [HttpPatch("{id}")] public IActionResult Update(int id, [FromBody] ToDOUpdateDTO updateDto) { var existingTodo = _todoRepository.GetById(id); if (existingTodo == null) return NotFound(); // 配置AutoMapper仅映射非空属性 _mapper.Map(updateDto, existingTodo); // 验证和保存逻辑... return Ok(existingTodo); }
AutoMapper配置示例:
CreateMap<ToDOUpdateDTO, ToDO>() .ForMember(dest => dest.Name, opt => opt.Condition(src => !string.IsNullOrEmpty(src.Name))) .ForMember(dest => dest.Description, opt => opt.Condition(src => !string.IsNullOrEmpty(src.Description)));
内容的提问来源于stack exchange,提问作者Piyush
相关产品推荐
相关产品推荐

