C# XML差异对比
直接可用,使用灵活
public class XmlDiffer{#region private objectprivate readonly XmlDocument sourceDoc = new XmlDocument();private readonly XmlDocument destinationDoc = new XmlDocument();private readonly StringBuilder report = new StringBuilder();#endregion#region constructorprivate readonly bool compareAttributes;private readonly bool compareContent;private readonly bool compareOrder;private readonly bool caseSensitive;private readonly HashSet<string> ignoredPaths;private readonly List<DiffRecord> diffRecords = new List<DiffRecord>();private readonly List<DiffTypeSummary> diffSummary = new List<DiffTypeSummary>();#endregion#region public objectpublic enum DiffType{ELEMENT_NAME_MISMATCH,ELEMENT_NAME_CASE_MISMATCH,CONTENT_MISMATCH,CHILD_ORDER_MISMATCH,MISSING_ATTRIBUTE,ATTRIBUTE_VALUE_MISMATCH,MISSING_ELEMENT}public enum ReportFormat{TXT,XML,JSON}public class CompareResult{public bool CompareFlag{get{return Summary == null ? false : (Summary.Count == 0 ? true : false);}}public List<DiffTypeSummary> Summary { get; set; }public List<DiffRecord> Diff { get; set; }}public class DiffTypeSummary{public string DiffType { get; set; }public int Count { get; set; }}public class DiffRecord{public string Path { get; set; }public string DiffType { get; set; }public string Expected { get; set; }public string Actual { get; set; }public string Suggestion { get; set; }}public ReportFormat reportFormat { get; set; } = ReportFormat.TXT;public CompareResult compareResult { get; set; }#endregion#region Public Functionpublic XmlDiffer(string sourceXml, string destinationXml, bool compareAttributes = false, bool compareContent = false, bool compareOrder = false, bool caseSensitive = true, List<string> ignoredPaths = null, bool isFilePath = false){if (isFilePath){sourceDoc.Load(sourceXml);destinationDoc.Load(destinationXml);}else{sourceDoc.LoadXml(sourceXml);destinationDoc.LoadXml(destinationXml);}this.compareAttributes = compareAttributes;this.compareContent = compareContent;this.compareOrder = compareOrder;this.ignoredPaths = ignoredPaths != null ? new HashSet<string>(ignoredPaths) : new HashSet<string>();this.reportFormat = reportFormat;this.caseSensitive = caseSensitive;}public string Compare(){CompareNodes(sourceDoc.DocumentElement, destinationDoc.DocumentElement, sourceDoc.DocumentElement.Name);AppendStats();GetResultModel();return GetReportString(reportFormat);}#endregion#region Generate Reportpublic string GetReportString(ReportFormat outFormat = ReportFormat.TXT){switch (outFormat){case ReportFormat.JSON:return GenerateJsonReport();case ReportFormat.XML:return GenerateXmlReport();case ReportFormat.TXT:default:return report.ToString();}}private void GetResultModel(){CompareResult result = new CompareResult();result.Summary = diffSummary;result.Diff = diffRecords;compareResult = result;}private string GenerateJsonReport(){return Newtonsoft.Json.JsonConvert.SerializeObject(compareResult, Newtonsoft.Json.Formatting.Indented);}private string GenerateXmlReport(){var serializer = new XmlSerializer(typeof(CompareResult));using (var writer = new StringWriter()){serializer.Serialize(writer, compareResult);return writer.ToString();}}public void SaveReport(string filePath, ReportFormat outFormat = ReportFormat.TXT){var content = GetReportString(outFormat);File.WriteAllText(filePath, content, Encoding.UTF8);}#endregion#region Compare XMLprivate void CompareNodes(XmlNode sourceNode, XmlNode destNode, string path){if (ignoredPaths.Contains(path)) return;if (sourceNode != null && destNode != null){if (!string.Equals(sourceNode.Name, destNode.Name, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)){AppendMismatch(path, DiffType.ELEMENT_NAME_MISMATCH, sourceNode.Name, destNode.Name, $"缺失节点 {sourceNode.Name}");}if (compareAttributes){CompareAttributes(sourceNode, destNode, path);}if (compareContent && NodeHasChildNodes(sourceNode) == false && NodeHasChildNodes(destNode) == false){string sourceText = sourceNode.InnerText.Trim();string destText = destNode.InnerText.Trim();if (!string.IsNullOrEmpty(sourceText) || !string.IsNullOrEmpty(destText)){if (sourceText != destText){AppendMismatch(path, DiffType.CONTENT_MISMATCH, sourceText, destText, "请检查文本内容差异或修改文本内容为期望值");}}}var sourceChildren = GetChildElements(sourceNode);var destChildren = GetChildElements(destNode);if (compareOrder && !IsSameOrder(sourceChildren, destChildren)){AppendMismatch(path, DiffType.CHILD_ORDER_MISMATCH, "", "顺序不同", "检查子节点顺序");}var allKeys = new HashSet<string>(sourceChildren.Keys);allKeys.UnionWith(destChildren.Keys);var processedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var key in allKeys.ToList()) {if (processedKeys.Contains(key)) continue;sourceChildren.TryGetValue(key, out var sList);destChildren.TryGetValue(key, out var dList);int maxCount = Math.Max(sList?.Count ?? 0, dList?.Count ?? 0);for (int i = 0; i < maxCount; i++){XmlNode sChild = i < (sList?.Count ?? 0) ? sList[i] : null;XmlNode dChild = i < (dList?.Count ?? 0) ? dList[i] : null;string newPath = GetUniquePath(sChild ?? dChild, i, path);if (ignoredPaths.Contains(newPath)) continue;if (sChild != null && dChild != null){CompareNodes(sChild, dChild, newPath);}else if (sChild != null){if (caseSensitive && TryMatchCaseInsensitive(key, destChildren, i, out var dCaseMismatch)){AppendMismatch(newPath, DiffType.ELEMENT_NAME_CASE_MISMATCH, key, dCaseMismatch.Name, "目标 XML 中节点名称大小写不一致,请统一大小写");processedKeys.Add(key);processedKeys.Add(dCaseMismatch.Name);continue;}AppendMissingElement(newPath, DiffType.MISSING_ELEMENT, sChild);}else if (dChild != null){if (caseSensitive && TryMatchCaseInsensitive(key, sourceChildren, i, out var sCaseMismatch)){AppendMismatch(newPath, DiffType.ELEMENT_NAME_CASE_MISMATCH, sCaseMismatch.Name, key, "目标 XML 中节点名称大小写不一致,请统一大小写");processedKeys.Add(key);processedKeys.Add(sCaseMismatch.Name);continue;}AppendMismatch(newPath, DiffType.ELEMENT_NAME_MISMATCH, "", dChild.Name, "目标 XML 中存在多余节点,请确认是否需要删除");}}}}else if (sourceNode != null){AppendMissingElement(path, DiffType.MISSING_ELEMENT, sourceNode);}else{AppendMismatch(path, DiffType.ELEMENT_NAME_MISMATCH, "", destNode.Name, "请手动检查此节点");}}private void CompareAttributes(XmlNode sourceNode, XmlNode destNode, string path){var sourceAttrs = sourceNode.Attributes;var destAttrs = destNode.Attributes;if (sourceAttrs == null) return;if (destNode.Name == "MsgBus"){}foreach (XmlAttribute sourceAttr in sourceAttrs){var destAttr = destAttrs?[sourceAttr.Name];if (destAttr == null){AppendMismatch(path, DiffType.MISSING_ATTRIBUTE, sourceAttr.Name, "", $"建议: 添加属性 {sourceAttr.Name}=\"{sourceAttr.Value}\"");}else if (sourceAttr.Value != destAttr.Value){AppendMismatch(path, DiffType.ATTRIBUTE_VALUE_MISMATCH, $"{sourceAttr.Name}=\"{sourceAttr.Value}\"", $"{destAttr.Name}=\"{destAttr.Value}\"", $"建议: 修改属性 {sourceAttr.Name} 为 \"{sourceAttr.Value}\"");}}}private Dictionary<string, List<XmlNode>> GetChildElements(XmlNode node){var dict = new Dictionary<string, List<XmlNode>>();foreach (XmlNode child in node.ChildNodes){if (child.NodeType == XmlNodeType.Element){string key = caseSensitive ? child.Name : child.Name.ToLower();if (!dict.ContainsKey(key))dict[key] = new List<XmlNode>();dict[key].Add(child);}}return dict;}private bool NodeHasChildNodes(XmlNode node){if (node.HasChildNodes && node.ChildNodes[0] != null && node.ChildNodes[0].Name != "#text"){return true;}else{return false;}}private bool IsSameOrder(Dictionary<string, List<XmlNode>> sourceChildren, Dictionary<string, List<XmlNode>> destChildren){var sourceList = sourceChildren.SelectMany(kvp => kvp.Value.Select(n => caseSensitive ? n.Name : n.Name.ToLower())).ToList();var destList = destChildren.SelectMany(kvp => kvp.Value.Select(n => caseSensitive ? n.Name : n.Name.ToLower())).ToList();return sourceList.SequenceEqual(destList);}private string GetUniquePath(XmlNode node, int index, string parentPath){string nodeName = caseSensitive ? node.Name : node.Name.ToLower();string attrInfo = node.Attributes?["id"]?.Value;string path = $"{parentPath}/{nodeName}[{index + 1}]";if (!string.IsNullOrEmpty(attrInfo))path += $"[@id='{attrInfo}']";return path;}private bool TryMatchCaseInsensitive(string key, Dictionary<string, List<XmlNode>> dict, int index, out XmlNode matchedNode){matchedNode = null;var matchKey = dict.Keys.FirstOrDefault(k =>string.Equals(k, key, StringComparison.OrdinalIgnoreCase) &&!string.Equals(k, key, StringComparison.Ordinal));if (matchKey != null && dict[matchKey].Count > index){matchedNode = dict[matchKey][index];return true;}return false;}#endregion#region Append differenceprivate void AppendMismatch(string path, DiffType type, string expected, string actual, string suggestion){AddDiffRecord(path, type, expected, actual, suggestion);}private void AppendMissingElement(string path, DiffType type, XmlNode missingNode){string expected = missingNode.OuterXml;string actual = "Null";string suggestion = $"添加缺失元素: {expected}";AddDiffRecord(path, type, expected, actual, suggestion);}private void AppendStats(){report.AppendLine("📊 差异统计:");if (diffSummary.Count > 0){report.AppendLine($" ▸ 差异结果: 不匹配");}foreach (var kvp in diffSummary){report.AppendLine($" ▸ {kvp.DiffType}: {kvp.Count} 项");}report.AppendLine();}private void AddDiffRecord(string path, DiffType type, string expected, string actual, string suggestion){report.AppendLine($"✓ [{path}] {type}");report.AppendLine($" ▸ 期望: {expected}");report.AppendLine($" ▸ 实际: {actual}");report.AppendLine($" ▸ 建议: {suggestion}");report.AppendLine();if (diffSummary.Where(x => x.DiffType.Equals(type.ToString())).ToList().Count > 0){diffSummary.Where(s => s.DiffType == type.ToString()).ToList().ForEach(s => s.Count = s.Count + 1);}else{diffSummary.Add(new DiffTypeSummary { DiffType = type.ToString(), Count = 1 });}diffRecords.Add(new DiffRecord{Path = path,DiffType = type.ToString(),Expected = expected,Actual = actual,Suggestion = suggestion});}#endregion}