Last active
December 29, 2015 10:09
-
-
Save sweko/7654887 to your computer and use it in GitHub Desktop.
I always need to determine which objects from a collection that was round tripped to the UI were added, removed and changed.
The code is usually straight-forward, but repetitive. The ListDifference class is a helper with a simple usage that makes it easy to do that.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Given two collections, determines added, removed and changed items. | |
/// </summary> | |
public class ListDifference<T> | |
{ | |
/// <summary> | |
/// The items that were added to the collection | |
/// </summary> | |
public IEnumerable<T> AddedItems { get; private set; } | |
/// <summary> | |
/// The items that were removed from the collection | |
/// </summary> | |
public IEnumerable<T> RemovedItems { get; private set; } | |
/// <summary> | |
/// The items that are changed in the collection, according to the specified comparer. | |
/// </summary> | |
public IEnumerable<Tuple<T, T>> ChangedItems { get; private set; } | |
/// <summary> | |
/// The items that are not changed in the collection, according to the specified comparer. | |
/// If no comparer is specified, all items with matching keys are considered unchanged. | |
/// </summary> | |
public IEnumerable<Tuple<T, T>> UnchangedItems { get; private set; } | |
/// <summary> | |
/// Creates a new differences object between an original collection and an updated version | |
/// </summary> | |
/// <param name="original">The original values of the collection</param> | |
/// <param name="updated">The updated values of the collection</param> | |
/// <param name="keyExtractor">An expression that extracts the key from an object</param> | |
/// <param name="comparer">An expression that compares two objects</param> | |
public ListDifference(IEnumerable<T> original, IEnumerable<T> updated, Func<T, object> keyExtractor = null, Func<T, T, bool> comparer = null) | |
{ | |
if (keyExtractor == null) | |
keyExtractor = t => t; | |
AddedItems = updated.Difference(original, keyExtractor); | |
RemovedItems = original.Difference(updated, keyExtractor); | |
var matches = from u in updated | |
from o in original | |
where keyExtractor(u).Equals(keyExtractor(o)) | |
select Tuple.Create(o, u); | |
if (comparer == null) | |
{ | |
//we are unable to detect changed items | |
ChangedItems = null; | |
UnchangedItems = matches; | |
} | |
else | |
{ | |
ChangedItems = matches.Where(t => !comparer(t.Item1, t.Item2)); | |
UnchangedItems = matches.Where(t => comparer(t.Item1, t.Item2)); | |
} | |
} | |
} | |
/// <summary> | |
/// Given two collections, determines added, removed and changed items. | |
/// Static version for using type inference when calling. | |
/// </summary> | |
public static class ListDifference | |
{ | |
/// <summary> | |
/// Given two collections, determines added, removed and changed items. | |
/// </summary> | |
/// <typeparam name="T">The type of the elements of the collections</typeparam> | |
/// <param name="original">The original values of the collection</param> | |
/// <param name="updated">The updated values of the collection</param> | |
/// <param name="keyExtractor">An expression that extracts the key from an object</param> | |
/// <param name="comparer">An expression that compares two objects</param> | |
/// <returns>The differences object between an original collection and an updated version</returns> | |
public static ListDifference<T> Difference<T>(IEnumerable<T> original, IEnumerable<T> updated, Func<T, object> keyExtractor = null, Func<T, T, bool> comparer = null) | |
{ | |
return new ListDifference<T>(original, updated, keyExtractor, comparer); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
void Main() | |
{ | |
Console.WriteLine("Persons example"); | |
var csiOriginal = new List<Person> | |
{ | |
new Person {ID = 1, FirstName = "William", LastName = "Petersen"}, | |
new Person {ID = 2, FirstName = "Marg", LastName = "Helgenberger"}, | |
new Person {ID = 3, FirstName = "Jorja", LastName = "Fox"}, | |
new Person {ID = 4, FirstName = "Gary", LastName = "Dourdan"}, | |
new Person {ID = 5, FirstName = "Eric", LastName = "Szmanda"}, | |
}; | |
var csiUpdated = new List<Person> | |
{ | |
new Person {ID = 3, FirstName = "Jorja-An", LastName = "Fox"}, | |
new Person {ID = 5, FirstName = "Eric", LastName = "Szmanda"}, | |
new Person {ID = 6, FirstName = "Ted", LastName = "Danson"}, | |
new Person {ID = 7, FirstName = "Elisabeth", LastName = "Shue"}, | |
}; | |
//detect changes in first name, as well as added and removed items | |
var differences = ListDifference.Difference(csiOriginal, csiUpdated, p => p.ID, (f,s) => f.FirstName.Equals(s.FirstName)); | |
Console.WriteLine("Added persons"); | |
foreach(Person p in differences.AddedItems) | |
{ | |
Console.WriteLine(p.FirstName); | |
} | |
Console.WriteLine(); | |
Console.WriteLine("Removed persons"); | |
foreach(Person p in differences.RemovedItems) | |
{ | |
Console.WriteLine(p.FirstName); | |
} | |
Console.WriteLine(); | |
Console.WriteLine("Changed persons"); | |
foreach(var tuple in differences.ChangedItems) | |
{ | |
Console.WriteLine(string.Format("First: {0}, Second: {1}", tuple.Item1.FirstName, tuple.Item2.FirstName)); | |
} | |
Console.WriteLine(); | |
Console.WriteLine("Unchanged persons"); | |
foreach(var tuple in differences.UnchangedItems) | |
{ | |
Console.WriteLine(string.Format("First: {0}, Second: {1}", tuple.Item1.FirstName, tuple.Item2.FirstName)); | |
} | |
Console.WriteLine(); | |
Console.WriteLine(); | |
Console.WriteLine("Integers example"); | |
int[] integersOriginal = {1, 2, 3, 4, 5}; | |
int[] integersUpdated = {2, 4, 6, 8}; | |
//whole object is used for comparison | |
var intDifferences = ListDifference.Difference(integersOriginal, integersUpdated); | |
Console.WriteLine("Added items"); | |
foreach(int i in intDifferences.AddedItems) | |
{ | |
Console.WriteLine(i); | |
} | |
Console.WriteLine(); | |
Console.WriteLine("Removed items"); | |
foreach(int i in intDifferences.RemovedItems) | |
{ | |
Console.WriteLine(i); | |
} | |
Console.WriteLine(); | |
Console.WriteLine("Unchanged items"); | |
foreach(var tuple in intDifferences.UnchangedItems) | |
{ | |
Console.WriteLine(string.Format("First: {0}, Second: {1}", tuple.Item1, tuple.Item2)); | |
} | |
} | |
public class Person | |
{ | |
public int ID {get; set;} | |
public string FirstName {get; set;} | |
public string LastName {get; set;} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// IEnumerable extensions. | |
/// </summary> | |
public static class IEnumerableExtensions | |
{ | |
/// <summary> | |
/// Produces the set difference of two sequences by using the key extractor to compare keys. | |
/// </summary> | |
/// <typeparam name="T">The type of the elements of the input sequences</typeparam> | |
/// <param name="first">A collection whose elements that are not matched in second will be returned.</param> | |
/// <param name="second">A collection whose elements that match in the first sequence will cause those elements to be removed from the returned sequence.</param> | |
/// <param name="keyExtractor">The key extractor that return the key to be matched.</param> | |
/// <returns>A sequence that contains the set difference of the elements of two sequences</returns> | |
public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> first, IEnumerable<T> second, Func<T, TKey> keyExtractor) | |
{ | |
return first.Except(second, new KeyEqualityComparer<T, TKey>(keyExtractor)); | |
} | |
/// <summary> | |
/// Produces the set difference of two sequences by using the key extractor to compare keys. Alias for the Except method | |
/// </summary> | |
/// <typeparam name="T">The type of the elements of the input sequences</typeparam> | |
/// <param name="first">A collection whose elements that are not matched in second will be returned.</param> | |
/// <param name="second">A collection whose elements that match in the first sequence will cause those elements to be removed from the returned sequence.</param> | |
/// <param name="keyExtractor">The key extractor that return the key to be matched.</param> | |
/// <returns>A sequence that contains the set difference of the elements of two sequences</returns> | |
public static IEnumerable<T> Difference<T, TKey>(this IEnumerable<T> first, IEnumerable<T> second, Func<T, TKey> keyExtractor) | |
{ | |
return Except(first, second, keyExtractor); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Generic implementation of IEqualityComparer<T> that enables wrapping delegates in IEqualityComparer | |
/// Based on http://blog.lavablast.com/post/2010/05/05/Lambda-IEqualityComparer3cT3e.aspx | |
/// </summary> | |
public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T> | |
{ | |
private readonly Func<T, TKey> _keyExtractor; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="KeyEqualityComparer<T>"/> class. | |
/// Allows us to simply specify the key to compare with: x => x.ID | |
/// </summary> | |
/// <param name="keyExtractor">The key extractor.</param> | |
public KeyEqualityComparer(Func<T, TKey> keyExtractor) | |
{ | |
_keyExtractor = keyExtractor; | |
} | |
/// <summary> | |
/// Determines whether the specified objects have equal keys. | |
/// </summary> | |
/// <param name="x">The first object of type <paramref name="T"/> to compare.</param> | |
/// <param name="y">The second object of type <paramref name="T"/> to compare.</param> | |
/// <returns> | |
/// true if the specified objects are equal; otherwise, false. | |
/// </returns> | |
public bool Equals(T x, T y) | |
{ | |
return _keyExtractor(x).Equals(_keyExtractor(y)); | |
} | |
/// <summary> | |
/// Returns a hash code for this instance. | |
/// </summary> | |
/// <param name="obj">The object.</param> | |
/// <returns> | |
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. | |
/// </returns> | |
/// <exception cref="T:System.ArgumentNullException"> | |
/// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. | |
/// </exception> | |
public int GetHashCode(T obj) | |
{ | |
if (_keyExtractor == null) | |
return obj.GetHashCode(); | |
else | |
{ | |
var val = _keyExtractor(obj); | |
return val.GetHashCode(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment