csharp
复制代码
public class XmlDiffer
{
#region private object
private readonly XmlDocument sourceDoc = new XmlDocument();
private readonly XmlDocument destinationDoc = new XmlDocument();
private readonly StringBuilder report = new StringBuilder();
#endregion
#region constructor
private 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 object
public 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 string UniqueInfo { get; set; }
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 Function
/// <summary>
///
/// </summary>
/// <param name="sourceXml">基准XML</param>
/// <param name="destinationXml">与基准xml进行对比的xml</param>
/// <param name="compareAttributes">比较属性</param>
/// <param name="compareContent">比较内容</param>
/// <param name="compareOrder">比较顺序</param>
/// <param name="caseSensitive">大小写比较</param>
/// <param name="ignoredPaths">需要忽略的指定路径</param>
/// <param name="reportFormat">报告格式TXT,XML、Json)</param>
/// <param name="isFilePath">string or xml file path</param>
public 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 Report
public 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 XML
private 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); // 用于记录已处理大小写不一致的 key
foreach (var key in allKeys.ToList()) // 使用 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 difference
private 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 });
}
//if (!diffSummary.ContainsKey(type)) diffSummary[type] = 0;
//diffSummary[type]++;
// 结构化记录
diffRecords.Add(new DiffRecord
{
Path = path,
DiffType = type.ToString(),
Expected = expected,
Actual = actual,
Suggestion = suggestion
});
}
#endregion
}