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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 07:11:05