如何在.NET 9后端识别JSON部分更新中真正修改的属性?
区分显式设置Null与未传递属性的更新处理方案
背景
我开发了一个基于DevExtreme DataGrids的Web应用,支持用户更新行数据。前端通过.NET 9后端API发送更新请求:
[HttpPut] public async Task<IActionResult> MyPutMethodAsync([FromForm] int key, [FromForm] string values)
key:对应数据库数据的Idvalues:仅包含修改属性的JSON字符串
示例场景:
const myRow = {id: 4, name: "Alex", age: 20, nationality: "American", height: 180}; // 修改行数据 myRow.age = 21; myRow.height = 182; myRow.nationality = null; const key = myRow.id; const values = JSON.stringify({age: myRow.age, height: myRow.height, nationality: myRow.nationality});
核心问题
后端使用JsonConvert.PopulateObject将更新值填充到新对象时,无法区分:
- 用户显式设置为null的属性(如
nationality = null,需要同步到数据库) - 未在
values中传递的属性(如name,不应被视为修改)
直接填充后的对象会把未传递的属性设为null,导致错误更新。
优化方案
方案1:直接解析JObject精准更新实体
无需额外DTO或标记字段,直接解析JSON字符串获取所有提交的字段,只更新这些字段到数据库实体。
[HttpPut] public async Task<IActionResult> MyPutMethodAsync([FromForm] int key, [FromForm] string values) { var myRowEntity = await dbContext.MyTable.FindAsync(key); if (myRowEntity == null) return NotFound(); // 解析提交的JSON为JObject,获取所有实际变更的字段 var updateFields = JObject.Parse(values); foreach (var prop in updateFields.Properties()) { var entityProperty = dbContext.Entry(myRowEntity).Property(prop.Name); if (entityProperty != null) { // 显式设置字段值(包括null) entityProperty.CurrentValue = prop.Value.ToObject(entityProperty.Metadata.ClrType); } } await dbContext.SaveChangesAsync(); return Ok(); }
优势:
- 完全避免冗余代码,无需手动标记属性变更
- 精准控制更新范围,未提交的字段不会被触碰
- 自动区分显式null和未传递字段(未传递字段不会出现在JObject的属性列表中)
方案2:通用变更追踪DTO基类
如果必须使用DTO,可以创建通用基类统一处理属性变更追踪,避免每个属性写重复的标记代码。
1. 通用追踪基类
public abstract class TrackableDto { private readonly HashSet<string> _changedProperties = new HashSet<string>(); public IEnumerable<string> ChangedProperties => _changedProperties; protected void OnPropertyChanged(string propertyName) { _changedProperties.Add(propertyName); } protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (!EqualityComparer<T>.Default.Equals(field, value)) { field = value; OnPropertyChanged(propertyName); } } }
2. 业务DTO继承基类
public class MyRowClass : TrackableDto { public int Id { get; set; } private string _name; public string Name { get => _name; set => SetProperty(ref _name, value); } private int? _age; public int? Age { get => _age; set => SetProperty(ref _age, value); } private string _nationality; public string Nationality { get => _nationality; set => SetProperty(ref _nationality, value); } private int? _height; public int? Height { get => _height; set => SetProperty(ref _height, value); } }
3. 批量更新实体
[HttpPut] public async Task<IActionResult> MyPutMethodAsync([FromForm] int key, [FromForm] string values) { var updatedRow = new MyRowClass { Id = key }; JsonConvert.PopulateObject(values, updatedRow); var myRowEntity = await dbContext.MyTable.FindAsync(key); if (myRowEntity == null) return NotFound(); // 遍历所有变更属性,批量同步到实体 foreach (var propName in updatedRow.ChangedProperties) { var dtoProp = typeof(MyRowClass).GetProperty(propName); var entityProp = typeof(MyRowEntity).GetProperty(propName); if (dtoProp != null && entityProp != null) { var value = dtoProp.GetValue(updatedRow); entityProp.SetValue(myRowEntity, value); } } await dbContext.SaveChangesAsync(); return Ok(); }
优势:
- 消除重复的属性标记代码,所有DTO可复用基类逻辑
- 明确追踪哪些属性被实际修改
方案3:无查询更新实体(EF Core Attach)
如果不想从数据库查询实体,可以创建空实例并Attach到上下文,再标记变更字段:
[HttpPut] public async Task<IActionResult> MyPutMethodAsync([FromForm] int key, [FromForm] string values) { var myRowEntity = new MyRowEntity { Id = key }; dbContext.MyTable.Attach(myRowEntity); var updateFields = JObject.Parse(values); JsonConvert.PopulateObject(values, myRowEntity); // 标记所有提交的字段为已修改 foreach (var prop in updateFields.Properties()) { dbContext.Entry(myRowEntity).Property(prop.Name).IsModified = true; } await dbContext.SaveChangesAsync(); return Ok(); }
优势:减少一次数据库查询,适合简单实体场景。
推荐方案
优先选择方案1,它最简洁高效,无需额外代码结构,直接针对提交的字段进行精准更新,完美解决显式null和未传递字段的区分问题。
内容的提问来源于stack exchange,提问作者T. Jami
相关产品推荐
相关产品推荐

