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

如何在不使用Entity Framework的ASP.NET Core中追踪模型值变更?

问题背景

我正尝试将一个严重依赖基于INotifyProperty的数据绑定来追踪并记录模型变更、从而将单独的“值变更”审计信息写入另一张表的WinForms应用迁移至ASP.NET Core。
已搜索许久,但找到的解决方案均涉及Entity Framework的示例。
已将数据绑定到表单,并可通过IFormCollection对象、TryUpdateModelAsync(model)或直接将模型传入PostFunction的方式获取更新后的信息。然而,除了获取原始模型和变更后模型的副本再逐个属性对比(这远非理想方案)外,找不到仅识别变更值的方法。

请问这是否可行?有无最佳实践?若有,能否提供可靠的参考资料?

当前ASP.NET Core获取数据的Post函数示例

public IActionResult Post_Client(ClientDataModel client) {
    Console.WriteLine(client.Surname);
    return RedirectToAction("Workspace");
}

public IActionResult Post_Client(IFormCollection formData) {
    var client = new ClientDataModel();
    client.Surname = formData.FirstOrDefault(a => a.Key == "Surname").Value;
    Console.WriteLine(client.Surname);
    return RedirectToAction("Workspace");
}

public IActionResult Post_Client() {
    var client = new ClientDataModel();
    TryUpdateModelAsync(client);
    Console.WriteLine(client.Surname);
    return RedirectToAction("Workspace");
}

WinForms中的实现方式

public class ClientDataModel{
    private string _surname;
    public string Surname { get => _surname; set => _surname = value.Truncate(50); }
}

public class ClientBindingHelper:BaseNotifier{
    private ClientDataModel objmodel;
    public ClientDataModel Model {
        get { return this.objmodel; }
        set { this.objmodel = value; }
    }
    public string Surname {
        get { return this.Model.Surname; }
        set {
            this.Model.LogChange("Surname", this.Model.Surname, value);
            this.Model.Surname = value;
            base.NotifyPropertyChanged("Surname");
        }
    }
}

WinForms表单绑定代码

Private Sub BindData()
    Dim helper = New ClientBindingHelper()
    txtSurname.DataBindings.Add("Text", myBindingHelper, "Surname")    
End Sub

解决方案与最佳实践

1. 可行性结论

完全可行,无需依赖EF,也不用全量对比原始/新模型,通过拦截更新过程或增强模型自身能力即可实现变更追踪。

2. 方案一:增强模型的内置变更追踪(最贴近WinForms思路)

给模型添加基类,在属性setter中自动记录变更,逻辑内聚,维护成本低:

public abstract class TrackableModel
{
    private Dictionary<string, (object OldValue, object NewValue)> _changes = new();
    // 对外暴露只读的变更记录
    public IReadOnlyDictionary<string, (object OldValue, object NewValue)> Changes => _changes.AsReadOnly();

    protected void TrackChange(string propertyName, object oldValue, object newValue)
    {
        if (!Equals(oldValue, newValue))
        {
            _changes[propertyName] = (oldValue, newValue);
        }
    }

    // 重置变更记录,用于保存后清理
    public void ResetChanges() => _changes.Clear();
}

public class ClientDataModel : TrackableModel
{
    private string _surname;
    public string Surname 
    { 
        get => _surname; 
        set 
        {
            // 先记录变更再赋值
            TrackChange(nameof(Surname), _surname, value);
            _surname = value.Truncate(50); 
        }
    }
}

// 在Post接口中使用
public async Task<IActionResult> Post_Client(int id)
{
    // 从数据库获取原始模型
    var originalClient = await _clientRepository.GetByIdAsync(id);
    // 直接更新原始模型,触发属性setter中的追踪逻辑
    await TryUpdateModelAsync(originalClient);

    // 遍历变更记录写入审计表
    foreach (var change in originalClient.Changes)
    {
        _auditRepository.LogChange(
            entityType: "Client",
            entityId: id,
            propertyName: change.Key,
            oldValue: change.Value.OldValue,
            newValue: change.Value.NewValue
        );
    }

    await _clientRepository.SaveChangesAsync();
    originalClient.ResetChanges();
    return RedirectToAction("Workspace");
}

3. 方案二:通用模型更新拦截器(适合无法修改模型的场景)

封装工具类,统一处理模型更新与变更记录,复用性强:

public static class ModelUpdateTracker
{
    public static bool UpdateAndTrack<T>(T originalModel, T newModel, Action<string, object, object> logChangeAction)
        where T : class
    {
        var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        bool hasChanges = false;

        foreach (var prop in properties)
        {
            var originalVal = prop.GetValue(originalModel);
            var newVal = prop.GetValue(newModel);

            if (!Equals(originalVal, newVal))
            {
                logChangeAction(prop.Name, originalVal, newVal);
                prop.SetValue(originalModel, newVal);
                hasChanges = true;
            }
        }

        return hasChanges;
    }
}

// 在Post接口中使用
public async Task<IActionResult> Post_Client(int id)
{
    var originalClient = await _clientRepository.GetByIdAsync(id);
    var updatedClient = new ClientDataModel();
    await TryUpdateModelAsync(updatedClient);

    // 调用工具类更新并追踪变更
    ModelUpdateTracker.UpdateAndTrack(originalClient, updatedClient, 
        (propName, oldVal, newVal) => 
        {
            _auditRepository.LogChange("Client", id, propName, oldVal, newVal);
        });

    await _clientRepository.SaveChangesAsync();
    return RedirectToAction("Workspace");
}

4. 方案三:利用ModelState追踪提交的变更

通过ASP.NET Core内置的ModelState获取表单提交的所有字段,对比原始模型值:

public async Task<IActionResult> Post_Client(int id)
{
    var originalClient = await _clientRepository.GetByIdAsync(id);
    // 将ModelState转换为键值对(字段名-提交值)
    var formValues = ModelState.ToDictionary(kv => kv.Key, kv => kv.Value.AttemptedValue);

    foreach (var prop in typeof(ClientDataModel).GetProperties())
    {
        // 匹配表单字段名与模型属性名(需保证前端字段名和属性名一致,或添加映射逻辑)
        if (formValues.TryGetValue(prop.Name, out var submittedValue))
        {
            var originalVal = prop.GetValue(originalClient);
            // 转换提交值的类型以匹配属性类型
            var convertedNewVal = Convert.ChangeType(submittedValue, prop.PropertyType);

            if (!Equals(originalVal, convertedNewVal))
            {
                _auditRepository.LogChange("Client", id, prop.Name, originalVal, convertedNewVal);
                prop.SetValue(originalClient, convertedNewVal);
            }
        }
    }

    await _clientRepository.SaveChangesAsync();
    return RedirectToAction("Workspace");
}

5. 最佳实践总结

  • 优先选择增强模型内置追踪的方案,逻辑和WinForms的BindingHelper思路一致,代码内聚性强,后续维护成本低。
  • 若无法修改现有模型,使用通用更新拦截器,一次封装可复用在所有模型上。
  • 避免事后全量对比模型,尤其是字段较多的实体,性能和维护性都较差。
  • 审计记录需包含核心信息:实体类型、实体ID、属性名、旧值、新值、操作时间、操作人,便于后续溯源。

内容的提问来源于stack exchange,提问作者DazzaJ

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 21:55:57