SmartObjectDiffKit is a lightweight, high-performance, and enterprise-grade object comparison library for .NET. It allows you to compare any two object graphs (e.g. models, POCOs, collections, Dictionaries) and produce detailed, highly-configurable, human-readable, and machine-readable difference reports.
Optimized for low allocation and high throughput, it is ideal for audit logging, version history tracking, change detection, and automated integration testing.
- 🔍 Deep Comparison: Traversers deep nested object graphs, collections, and dictionaries.
- 🏷️ Custom Display Names: Customize property paths using
[DiffDisplayName("Custom Name")]. - 🔑 Key-Based Collections: Align elements in collections automatically by key property using
[DiffKey]. - ⚡ High Performance: Compiles and caches reflection getters and type metadata to minimize runtime overhead.
- 🔀 Order-Insensitive Matching: Match collection elements structurally or by key, disregarding their index position.
- 📝 Multiple Exporters: Generate output in JSON, XML, Markdown, HTML, CSV, Plain Text, and Console Colorized formats.
- 🛡️ Zero Dependencies: Core library targets .NET Standard 2.0 with no external dependencies.
- 🔄 Circular Reference Detection: Built-in protection against infinite recursion.
Install via NuGet Package Manager CLI:
dotnet add package SmartObjectDiffKitCompare two simple objects using the default configuration:
usingSmartObjectDiffKit;varoldUser=new{Name="John Doe",Age=30};varnewUser=new{Name="Jane Doe",Age=31};// Compare objects and get resultDiffResultresult=ObjectDiffer.Create().Compare(oldUser,newUser);Console.WriteLine(result.IsEqual);// FalseConsole.WriteLine(result.DifferenceCount);// 2foreach(vardiffinresult.Differences){Console.WriteLine($"[{diff.DifferenceType}] {diff.PropertyPath}: '{diff.OldValue}' -> '{diff.NewValue}'");}// Output:// [Modified] Name: 'John Doe' -> 'Jane Doe'// [Modified] Age: '30' -> '31'By default, lists are compared by index (ordered comparison). If you add, remove, or shuffle elements, this can lead to many incorrect modifications being reported.
By decorating an identity property with [DiffKey], the comparison engine automatically matches corresponding elements between the collections by their key:
publicclassOrderItem{[DiffKey]publicstringProductId{get;set;}=string.Empty;publicintQuantity{get;set;}}varoldItems=newList<OrderItem>{new(){ProductId="PROD-A",Quantity=5},new(){ProductId="PROD-B",Quantity=2}};varnewItems=newList<OrderItem>{// The items are swapped in position, and PROD-A quantity changednew(){ProductId="PROD-B",Quantity=2},new(){ProductId="PROD-A",Quantity=8}};varresult=ObjectDiffer.Create().Compare(oldItems,newItems);// It correctly matches PROD-A and reports the modified quantity, instead of mismatching elements!// PropertyPath will reflect the key: ["PROD-A"].Quantityvardiff=result.Differences[0];Console.WriteLine($"{diff.PropertyPath}: {diff.OldValue} -> {diff.NewValue}");// Output: ["PROD-A"].Quantity: 5 -> 8Use [DiffDisplayName] to change how properties are displayed in output reports (great for generating customer-facing audit logs):
publicclassEmployee{[DiffDisplayName("Job Title")]publicstringRole{get;set;}=string.Empty;[DiffDisplayName("Monthly Salary")]publicdecimalSalary{get;set;}}varresult=ObjectDiffer.Create().Compare(emp1,emp2);// If Salary changes, the PropertyPath in the report will be "Monthly Salary" instead of "Salary".Control the order in which properties are evaluated and displayed in output files:
publicclassProduct{[DiffOrder(1)]publicstringName{get;set;}=string.Empty;[DiffOrder(2)]publicdecimalPrice{get;set;}}You can ignore properties in multiple ways:
// 1. By decorating the property in codepublicclassAccount{[DiffIgnore]publicstringInternalToken{get;set;}=string.Empty;}// 2. By property name in configuration buildervardiffer=ObjectDiffer.Create().IgnoreProperty("LastModifiedDate").Build();// 3. By strong-typed lambda expressionvardiffer=ObjectDiffer.Create().IgnoreProperty<Account>(x =>x.InternalToken).Build();// 4. By generic predicate (e.g. ignore all properties starting with "Temp")vardiffer=ObjectDiffer.Create().IgnoreProperty((name,type)=>name.StartsWith("Temp")).Build();If elements do not have a defined [DiffKey], you can still check for equality without regarding order by enabling IgnoreCollectionOrder():
varoldList=newList<int>{1,2,3};varnewList=newList<int>{3,1,2};varresult=ObjectDiffer.Create().IgnoreCollectionOrder().Compare(oldList,newList);Console.WriteLine(result.IsEqual);// TrueNote: If complex objects do not have a [DiffKey], the engine will fall back to deep structural verification to match them.
SmartObjectDiffKit provides rich extensions to serialize your diff reports into various formats.
varresult=ObjectDiffer.Create().Compare(oldObj,newObj);stringjson=result.ToJson(indented:true);// System.Text.Json formatstringxml=result.ToXml();// Standard XML formatstringmarkdown=result.ToMarkdown();// Beautiful Markdown table reportstringhtml=result.ToHtml();// Styled responsive HTML reportstringcsv=result.ToCsv();// Comma-separated valuesstringtext=result.ToPlainText();// Clean tabular raw textstringconsole=result.ToConsole();// ANSI-friendly colorized console lines# Diff Report**Status:** Different
**Differences:** 1
**Elapsed Time:** 2.45ms
**Objects Compared:** 5
## Differences| Property Path | Type | Old Value | New Value | Severity ||---|---|---|---|---||`Address.City`| Modified | Springfield | Shelbyville | Medium |The library is completely thread-safe and optimized for production environments:
- Immutable Configuration:
ObjectDifferinstances are immutable once constructed. You can register them as Singletons in dependency injection containers. - Compiled Delegates: Caches compiled lambda getters inside
ConcurrentDictionaryto achieve near-native execution speed when reading property values dynamically. - Isolated Contexts: Each comparison call spawns a private state container (
ComparisonContext) ensuring thread isolation.
- SmartObjectDiffKit (Core Library):
.NET Standard 2.0(Runs on .NET Core, .NET 5/6/7/8/9/10, and .NET Framework 4.6.1+). - Benchmarks, Samples, and Tests:
.NET 10.0.
This project is licensed under the MIT License.