Skip to content

Repository files navigation

SmartObjectDiffKit

NuGet VersionLicense

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.


Key Features

  • 🔍 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.

Installation

Install via NuGet Package Manager CLI:

dotnet add package SmartObjectDiffKit

Quick Start

Compare 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'

Advanced Configurations

1. Key-Based Collection Comparison ([DiffKey])

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 -> 8

2. Custom Property Names ([DiffDisplayName])

Use [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".

3. Display Sorting ([DiffOrder])

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;}}

4. Ignoring Properties

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();

5. Ignoring Collection Order globally

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);// True

Note: If complex objects do not have a [DiffKey], the engine will fall back to deep structural verification to match them.


Exporting Reports

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

Example Markdown Output

# 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 |

Thread Safety & Caching

The library is completely thread-safe and optimized for production environments:

  • Immutable Configuration: ObjectDiffer instances are immutable once constructed. You can register them as Singletons in dependency injection containers.
  • Compiled Delegates: Caches compiled lambda getters inside ConcurrentDictionary to achieve near-native execution speed when reading property values dynamically.
  • Isolated Contexts: Each comparison call spawns a private state container (ComparisonContext) ensuring thread isolation.

Target Frameworks

  • 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.

License

This project is licensed under the MIT License.

About

A lightweight, high-performance, enterprise-grade object comparison library for .NET. Compare any object graph and produce detailed, configurable difference reports.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages