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

如何在.NET 9后端识别JSON部分更新中真正修改的属性?

区分显式设置Null与未传递属性的更新处理方案

背景

我开发了一个基于DevExtreme DataGrids的Web应用,支持用户更新行数据。前端通过.NET 9后端API发送更新请求:

[HttpPut]
public async Task<IActionResult> MyPutMethodAsync([FromForm] int key, [FromForm] string values)
  • key:对应数据库数据的Id
  • values:仅包含修改属性的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 00:02:33