如何在不使用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
相关产品推荐
相关产品推荐

