如何在C#中递归比较嵌套模型及集合以检测数据变更?
解决EF Code First模型嵌套及集合的差异检测问题
核心思路
要实现完整的模型差异检测,必须在原有反射逻辑基础上补充集合类型识别、嵌套对象递归对比、集合元素匹配校验这几个关键逻辑。直接对比集合引用必然会误判差异,得遍历元素逐一校验(通常依赖主键做元素匹配)。
具体实现步骤
1. 区分类型处理逻辑
首先要给不同类型的属性分情况处理:
- 值类型(如
int、string、DateTime)直接对比值是否相等 - 集合类型(实现
IEnumerable且不是字符串)进入集合专属对比逻辑 - 其他引用类型(比如嵌套的
EmploymentHistory)递归调用差异检测方法
2. 集合对比核心逻辑
集合对比的关键是先找到两个集合中对应的元素(优先用主键匹配,无主键可按顺序),再逐一校验元素内部属性:
- 先处理空集合情况:两个都为空/Null则无差异;一个空一个非空直接标记为新增/删除
- 用主键建立新旧集合的元素映射,区分出新增、删除、待校验的元素
- 对匹配上的元素递归对比内部属性,记录具体变更
3. 可复用工具类实现
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; public class ModelComparer { // 记录差异结果的实体类 public class DiffResult { public string PropertyPath { get; set; } public object OldValue { get; set; } public object NewValue { get; set; } public bool IsCollectionChange { get; set; } public string CollectionChangeType { get; set; } // Added/Removed/Modified } // 主对比方法 public List<DiffResult> Compare(object oldObj, object newObj, string parentPath = "") { var diffs = new List<DiffResult>(); if (oldObj == null && newObj == null) return diffs; if (oldObj == null || newObj == null) { diffs.Add(new DiffResult { PropertyPath = parentPath, OldValue = oldObj, NewValue = newObj, IsCollectionChange = false }); return diffs; } var type = oldObj.GetType(); if (type != newObj.GetType()) { diffs.Add(new DiffResult { PropertyPath = parentPath, OldValue = type.Name, NewValue = newObj.GetType().Name, IsCollectionChange = false }); return diffs; } // 处理集合类型 if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string)) { diffs.AddRange(CompareCollections((IEnumerable)oldObj, (IEnumerable)newObj, parentPath)); return diffs; } // 处理值类型和嵌套引用类型 foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!prop.CanRead) continue; var oldValue = prop.GetValue(oldObj); var newValue = prop.GetValue(newObj); var currentPath = string.IsNullOrEmpty(parentPath) ? prop.Name : $"{parentPath}.{prop.Name}"; // 值类型直接对比 if (prop.PropertyType.IsValueType || prop.PropertyType == typeof(string)) { if (!Equals(oldValue, newValue)) { diffs.Add(new DiffResult { PropertyPath = currentPath, OldValue = oldValue, NewValue = newValue, IsCollectionChange = false }); } continue; } // 递归处理嵌套对象 diffs.AddRange(Compare(oldValue, newValue, currentPath)); } return diffs; } // 集合对比方法 private List<DiffResult> CompareCollections(IEnumerable oldCol, IEnumerable newCol, string parentPath) { var diffs = new List<DiffResult>(); var oldList = oldCol.Cast<object>().ToList(); var newList = newCol.Cast<object>().ToList(); // 空集合特殊处理 if (!oldList.Any() && !newList.Any()) return diffs; if (!oldList.Any()) { diffs.Add(new DiffResult { PropertyPath = parentPath, OldValue = null, NewValue = newList.Count, IsCollectionChange = true, CollectionChangeType = "Added" }); return diffs; } if (!newList.Any()) { diffs.Add(new DiffResult { PropertyPath = parentPath, OldValue = oldList.Count, NewValue = null, IsCollectionChange = true, CollectionChangeType = "Removed" }); return diffs; } // 尝试获取主键属性(可扩展为读取[Key]特性) var idProp = oldList.First().GetType().GetProperty("Id"); if (idProp == null) { // 无主键时按顺序对比(仅适合有序集合) for (int i = 0; i < Math.Max(oldList.Count, newList.Count); i++) { var oldItem = i < oldList.Count ? oldList[i] : null; var newItem = i < newList.Count ? newList[i] : null; diffs.AddRange(Compare(oldItem, newItem, $"{parentPath}[{i}]")); } return diffs; } // 按主键建立元素映射 var oldDict = oldList.ToDictionary(item => idProp.GetValue(item)); var newDict = newList.ToDictionary(item => idProp.GetValue(item)); // 检测删除的元素 foreach (var key in oldDict.Keys.Except(newDict.Keys)) { diffs.Add(new DiffResult { PropertyPath = $"{parentPath}.{idProp.GetValue(oldDict[key])}", OldValue = oldDict[key], NewValue = null, IsCollectionChange = true, CollectionChangeType = "Removed" }); } // 检测新增的元素 foreach (var key in newDict.Keys.Except(oldDict.Keys)) { diffs.Add(new DiffResult { PropertyPath = $"{parentPath}.{idProp.GetValue(newDict[key])}", OldValue = null, NewValue = newDict[key], IsCollectionChange = true, CollectionChangeType = "Added" }); } // 检测修改的元素 foreach (var key in oldDict.Keys.Intersect(newDict.Keys)) { var itemDiffs = Compare(oldDict[key], newDict[key], $"{parentPath}.{key}"); if (itemDiffs.Any()) diffs.AddRange(itemDiffs); } return diffs; } }
4. 使用示例
对比Profile模型实例:
var oldProfile = new Profile { Id = 1, Name = "张三", EmploymentHistories = new List<EmploymentHistory> { new EmploymentHistory { Id = 1, Company = "旧公司" } } }; var newProfile = new Profile { Id = 1, Name = "张三", EmploymentHistories = new List<EmploymentHistory> { new EmploymentHistory { Id = 1, Company = "旧公司" } } }; var comparer = new ModelComparer(); var diffs = comparer.Compare(oldProfile, newProfile); if (!diffs.Any()) { Console.WriteLine("模型无差异"); } else { foreach (var diff in diffs) { var changeType = diff.CollectionChangeType ?? "修改"; Console.WriteLine($"路径:{diff.PropertyPath} | 变更类型:{changeType} | 旧值:{diff.OldValue} | 新值:{diff.NewValue}"); } }
5. 扩展优化方向
- 自定义主键识别:通过读取EF的
[Key]特性判断主键,替代硬编码的"Id" - 忽略指定属性:新增
[IgnoreDiff]自定义特性,反射时跳过标记的属性 - 有序集合校验:如果需要检测元素顺序变化,需结合索引位置做对比
- 性能优化:缓存反射获取的属性信息,减少重复反射开销
内容的提问来源于stack exchange,提问作者Jesus
相关产品推荐
相关产品推荐

