ObjectComparer 1.2.1
dotnet add package ObjectComparer --version 1.2.1
NuGet\Install-Package ObjectComparer -Version 1.2.1
<PackageReference Include="ObjectComparer" Version="1.2.1" />
<PackageVersion Include="ObjectComparer" Version="1.2.1" />
<PackageReference Include="ObjectComparer" />
paket add ObjectComparer --version 1.2.1
#r "nuget: ObjectComparer, 1.2.1"
#:package ObjectComparer@1.2.1
#addin nuget:?package=ObjectComparer&version=1.2.1
#tool nuget:?package=ObjectComparer&version=1.2.1
ObjectComparer
A flexible .NET library for comparing two objects of the same type and extracting differences based on configurable rules.
Overview
ObjectComparer allows you to define custom comparison rules for objects and their nested collections. Instead of prescribing specific difference types, the library is fully generic - you define your own difference classes that fit your domain needs.
Key Features:
- Rule-based comparison system with fluent API
- Simple property comparisons
- Collection comparisons with add/remove/change detection
- Two collection comparison strategies: flexible predicates or optimized key-based matching
- Recursive comparison for nested collections
- Fully generic - define your own difference types
- Targets .NET Standard 2.0 for broad compatibility
- 100% test coverage
- Comprehensive XML documentation
Installation
dotnet add package ObjectComparer
Or via NuGet Package Manager:
Install-Package ObjectComparer
Quick Start
using ObjectComparer;
// Define your difference type (interface or abstract class)
public interface IDifference { }
public class NameChanged : IDifference
{
public string OldName { get; }
public string NewName { get; }
public NameChanged(string oldName, string newName)
{
OldName = oldName;
NewName = newName;
}
}
// Create a comparer
IComparer<Person, IDifference> comparer = new Comparer<Person, IDifference>();
// Add comparison rules
comparer.AddRule(
condition: (source, target) => source.Name != target.Name,
differenceFactory: (source, target) => new NameChanged(source.Name, target.Name));
// Compare objects
var person1 = new Person { Name = "John" };
var person2 = new Person { Name = "Jane" };
IDifference[] differences = comparer.Compare(person1, person2);
Usage Guide
Simple Property Comparisons
Use AddRule() to compare simple properties:
comparer.AddRule(
condition: (source, target) => source.Age != target.Age,
differenceFactory: (source, target) => new AgeChanged(source.Age, target.Age));
comparer.AddRule(
condition: (source, target) => source.Email != target.Email,
differenceFactory: (source, target) => new EmailChanged(source.Email, target.Email));
Rules are evaluated in order and multiple rules can produce differences for the same comparison.
Collection Comparisons
ObjectComparer provides two approaches for comparing collections, each optimized for different scenarios:
Option 1: Key-Based Matching (Recommended for Performance)
Use when items have unique keys (like Id properties). This approach uses dictionary-based lookups for O(n) performance.
comparer.AddRuleForEach(
itemsSelector: person => person.Addresses,
keySelector: address => address.Id, // Must be unique!
addedFactory: (source, target, added) => new AddressAdded(added.Id, added.City),
removedFactory: (source, target, removed) => new AddressRemoved(removed.Id),
configureComparer: itemComparer => itemComparer
.AddRule(
condition: (sourceAddr, targetAddr) => sourceAddr.City != targetAddr.City,
differenceFactory: (sourceAddr, targetAddr) =>
new AddressCityChanged(sourceAddr.Id, sourceAddr.City, targetAddr.City)));
Performance: O(n) - uses dictionaries for efficient lookups
Requirement: Keys must be unique within each collection (throws ArgumentException if duplicates found)
Best for: Collections with natural unique identifiers (Id, Key, Code, etc.)
Option 2: Predicate-Based Matching (For Complex Logic)
Use when matching requires complex logic beyond simple key equality. This approach uses O(n²) performance but supports any matching logic.
comparer.AddRuleForEach(
itemsSelector: person => person.Addresses,
matchingPredicate: (sourceAddr, targetAddr) =>
sourceAddr.Id == targetAddr.Id, // Can be any complex logic
addedFactory: (source, target, added) => new AddressAdded(added.Id, added.City),
removedFactory: (source, target, removed) => new AddressRemoved(removed.Id),
configureComparer: itemComparer => itemComparer
.AddRule(
condition: (sourceAddr, targetAddr) => sourceAddr.City != targetAddr.City,
differenceFactory: (sourceAddr, targetAddr) =>
new AddressCityChanged(sourceAddr.Id, sourceAddr.City, targetAddr.City)));
Performance: O(n²) - iterates through items to find matches
Requirement: Predicate must uniquely identify items (throws InvalidOperationException if multiple matches found)
Best for:
- Composite keys:
(s, t) => s.FirstName == t.FirstName && s.LastName == t.LastName - Fuzzy matching:
(s, t) => s.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase) - Complex business logic for matching
Nested Comparers
The configureComparer parameter allows recursive comparison of matched items:
comparer.AddRuleForEach(
itemsSelector: company => company.Departments,
keySelector: dept => dept.Id,
configureComparer: deptComparer => deptComparer
// Compare department properties
.AddRule(
condition: (s, t) => s.Name != t.Name,
differenceFactory: (s, t) => new DepartmentNameChanged(s.Name, t.Name))
// Recursively compare employees within each department
.AddRuleForEach(
itemsSelector: dept => dept.Employees,
keySelector: emp => emp.Id,
configureComparer: empComparer => empComparer
.AddRule(
condition: (s, t) => s.Salary != t.Salary,
differenceFactory: (s, t) => new SalaryChanged(s.Id, s.Salary, t.Salary))));
Complete Example
using ObjectComparer;
public class Person
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public Address[] Addresses { get; set; } = Array.Empty<Address>();
}
public class Address
{
public int Id { get; set; }
public string City { get; set; } = string.Empty;
}
// Define your difference types
public interface IDifference { }
public class NameChanged : IDifference
{
public string OldName { get; }
public string NewName { get; }
public NameChanged(string oldName, string newName)
{
OldName = oldName;
NewName = newName;
}
}
public class AgeChanged : IDifference
{
public int OldAge { get; }
public int NewAge { get; }
public AgeChanged(int oldAge, int newAge)
{
OldAge = oldAge;
NewAge = newAge;
}
}
public class AddressAdded : IDifference
{
public int AddressId { get; }
public string City { get; }
public AddressAdded(int addressId, string city)
{
AddressId = addressId;
City = city;
}
}
public class AddressRemoved : IDifference
{
public int AddressId { get; }
public AddressRemoved(int addressId)
{
AddressId = addressId;
}
}
public class AddressCityChanged : IDifference
{
public int AddressId { get; }
public string OldCity { get; }
public string NewCity { get; }
public AddressCityChanged(int addressId, string oldCity, string newCity)
{
AddressId = addressId;
OldCity = oldCity;
NewCity = newCity;
}
}
// Usage
class Program
{
static void Main()
{
var person1 = new Person
{
Name = "John",
Age = 30,
Addresses = new[]
{
new Address { Id = 1, City = "New York" },
new Address { Id = 2, City = "Boston" }
}
};
var person2 = new Person
{
Name = "John",
Age = 31,
Addresses = new[]
{
new Address { Id = 1, City = "Los Angeles" },
new Address { Id = 3, City = "Chicago" }
}
};
// Create comparer with rules
IComparer<Person, IDifference> comparer = new Comparer<Person, IDifference>();
comparer.AddRule(
condition: (source, target) => source.Name != target.Name,
differenceFactory: (source, target) => new NameChanged(source.Name, target.Name));
comparer.AddRule(
condition: (source, target) => source.Age != target.Age,
differenceFactory: (source, target) => new AgeChanged(source.Age, target.Age));
// Use key-based matching for O(n) performance
comparer.AddRuleForEach(
itemsSelector: person => person.Addresses,
keySelector: address => address.Id,
addedFactory: (source, target, added) => new AddressAdded(added.Id, added.City),
removedFactory: (source, target, removed) => new AddressRemoved(removed.Id),
configureComparer: itemComparer => itemComparer
.AddRule(
condition: (sourceAddr, targetAddr) => sourceAddr.City != targetAddr.City,
differenceFactory: (sourceAddr, targetAddr) =>
new AddressCityChanged(sourceAddr.Id, sourceAddr.City, targetAddr.City)));
// Compare
IDifference[] differences = comparer.Compare(person1, person2);
// Results:
// - AgeChanged(30, 31)
// - AddressRemoved(2)
// - AddressAdded(3, "Chicago")
// - AddressCityChanged(1, "New York", "Los Angeles")
}
}
Performance Considerations
| Collection Size | Key-Based (keySelector) |
Predicate-Based (matchingPredicate) |
|---|---|---|
| 10 items | ~instant | ~instant |
| 100 items | ~instant | ~instant |
| 1,000 items | < 1ms | ~10ms |
| 10,000 items | ~10ms | ~1000ms (1 second) |
Recommendation: Use keySelector whenever possible. Only use matchingPredicate when you need complex matching logic that cannot be expressed as a simple key.
Building from Source
# Build the solution
dotnet build ObjectComparer.sln
# Run tests
dotnet test
# Run tests with coverage
dotnet test /p:CollectCoverage=true
# Run the demo workbench
dotnet run --project Workbench/Workbench.csproj
# Pack for NuGet
dotnet pack ObjectComparer/ObjectComparer.csproj -c Release
How It Works
The library uses a rule-based comparison pattern:
- You create a
Comparer<TType, TDiff>instance - You add rules via
AddRule()andAddRuleForEach() - Each rule implements the internal
IRuleinterface - When you call
Compare(), all rules are executed in order - All differences are aggregated and returned as an array
The Comparer class is split across multiple partial class files:
Comparer.cs- Main APIComparer.Rule.cs- Simple property comparison rulesComparer.RuleForEach.cs- Predicate-based collection rules (O(n²))Comparer.RuleForEachWithKey.cs- Key-based collection rules (O(n))Comparer.IRule.cs- Internal rule interface
License
MIT License - see LICENSE file for details
Author
Alkiviadis Skoutaris
Repository
https://github.com/askoutaris/object-comparer
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Set GenerateDocumentationFile to true so that nugets will contain documentation comments