Last active
November 4, 2021 14:33
-
-
Save ryanholden8/5602a6cc6decebb5bb35e21aa37efa67 to your computer and use it in GitHub Desktop.
Immutable Collection Types That Support Deep Equality (vs the default reference check equality)
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
using System.Collections.Generic; | |
using System.Linq; | |
namespace System.Collections.Immutable | |
{ | |
[System.Text.Json.Serialization.JsonConverter(typeof(JsonConverterForImmutableArrayWithDeepEqualityFactory))] | |
public struct ImmutableArrayWithDeepEquality<T> : IEquatable<ImmutableArrayWithDeepEquality<T>>, IEnumerable, IEnumerable<T> | |
{ | |
private readonly ImmutableArray<T> _list; | |
public ImmutableArrayWithDeepEquality(ImmutableArray<T> list) => _list = list; | |
#region ImmutableArray Implementation | |
public T this[int index] => _list[index]; | |
public int Count => _list.Length; | |
public ImmutableArrayWithDeepEquality<T> Add(T value) => _list.Add(value).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> AddRange(IEnumerable<T> items) => _list.AddRange(items).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> Clear() => _list.Clear().WithDeepEquality(); | |
public ImmutableArray<T>.Enumerator GetEnumerator() => _list.GetEnumerator(); | |
public int IndexOf(T item, int index, int count, IEqualityComparer<T> equalityComparer) => _list.IndexOf(item, index, count, equalityComparer); | |
public ImmutableArrayWithDeepEquality<T> Insert(int index, T element) => _list.Insert(index, element).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> InsertRange(int index, IEnumerable<T> items) => _list.InsertRange(index, items).WithDeepEquality(); | |
public int LastIndexOf(T item, int index, int count, IEqualityComparer<T> equalityComparer) => _list.LastIndexOf(item, index, count, equalityComparer); | |
public ImmutableArrayWithDeepEquality<T> Remove(T value, IEqualityComparer<T> equalityComparer) => _list.Remove(value, equalityComparer).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> RemoveAll(Predicate<T> match) => _list.RemoveAll(match).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> RemoveAt(int index) => _list.RemoveAt(index).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> RemoveRange(IEnumerable<T> items, IEqualityComparer<T> equalityComparer) => _list.RemoveRange(items, equalityComparer).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> RemoveRange(int index, int count) => _list.RemoveRange(index, count).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> Replace(T oldValue, T newValue, IEqualityComparer<T> equalityComparer) => _list.Replace(oldValue, newValue, equalityComparer).WithDeepEquality(); | |
public ImmutableArrayWithDeepEquality<T> SetItem(int index, T value) => _list.SetItem(index, value).WithDeepEquality(); | |
public bool IsDefaultOrEmpty => _list.IsDefaultOrEmpty; | |
public static ImmutableArrayWithDeepEquality<T> Empty = new(ImmutableArray<T>.Empty); | |
#endregion | |
#region IEnumerable | |
IEnumerator IEnumerable.GetEnumerator() => (_list as IEnumerable).GetEnumerator(); | |
IEnumerator<T> IEnumerable<T>.GetEnumerator() => (_list as IEnumerable<T>).GetEnumerator(); | |
#endregion | |
#region IEquatable | |
public bool Equals(ImmutableArrayWithDeepEquality<T> other) => _list.SequenceEqual(other); | |
public override bool Equals(object obj) => obj is ImmutableArrayWithDeepEquality<T> other && Equals(other); | |
public static bool operator ==(ImmutableArrayWithDeepEquality<T>? left, ImmutableArrayWithDeepEquality<T>? right) => left is null ? right is null : left.Equals(right); | |
public static bool operator !=(ImmutableArrayWithDeepEquality<T>? left, ImmutableArrayWithDeepEquality<T>? right) => !(left == right); | |
public override int GetHashCode() | |
{ | |
unchecked | |
{ | |
return _list.Aggregate(19, (h, i) => h * 19 + i!.GetHashCode()); | |
} | |
} | |
#endregion | |
} | |
public static class ImmutableArrayWithDeepEqualityEx | |
{ | |
public static ImmutableArrayWithDeepEquality<T> WithDeepEquality<T>(this ImmutableArray<T> list) => new(list); | |
public static ImmutableArrayWithDeepEquality<T> ToImmutableArrayWithDeepEquality<T>(this IEnumerable<T> list) => new(list.ToImmutableArray()); | |
} | |
} |
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
using System.Collections.Generic; | |
using Xunit; | |
namespace System.Collections.Immutable.Tests | |
{ | |
public class ImmutableDeepEqualityTests | |
{ | |
[Fact] | |
public void ArraysWithSameValues_AreConsideredEqual() | |
{ | |
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality(); | |
var array1Copy = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality(); | |
Assert.Equal(array1, array1Copy); | |
Assert.True(array1 == array1Copy); | |
Assert.False(array1 != array1Copy); | |
Assert.True(array1.Equals(array1Copy)); | |
} | |
[Fact] | |
public void ArraysWithDifferentValues_AreConsideredNotEqual() | |
{ | |
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality(); | |
var array2 = new int[] { 4, 5, 6 }.ToImmutableArrayWithDeepEquality(); | |
Assert.NotEqual(array1, array2); | |
Assert.False(array1 == array2); | |
Assert.True(array1 != array2); | |
Assert.False(array1.Equals(array2)); | |
} | |
[Fact] | |
public void DictionariesWithSameValues_AreConsideredEqual() | |
{ | |
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality(); | |
var dict1Copy = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality(); | |
Assert.Equal(dict1, dict1Copy); | |
Assert.True(dict1 == dict1Copy); | |
Assert.False(dict1 != dict1Copy); | |
Assert.True(dict1.Equals(dict1Copy)); | |
} | |
[Fact] | |
public void DictionariesWithDifferentKeys_AreConsideredNotEqual() | |
{ | |
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality(); | |
var dict2 = new Dictionary<string, string> { { "KeyA", "1" }, { "KeyB", "2" } }.ToImmutableDictionaryWithDeepEquality(); | |
Assert.NotEqual(dict1, dict2); | |
Assert.False(dict1 == dict2); | |
Assert.True(dict1 != dict2); | |
Assert.False(dict1.Equals(dict2)); | |
} | |
[Fact] | |
public void DictionariesWithDifferentValues_AreConsideredNotEqual() | |
{ | |
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality(); | |
var dict2 = new Dictionary<string, string> { { "Key1", "A" }, { "Key2", "B" } }.ToImmutableDictionaryWithDeepEquality(); | |
Assert.NotEqual(dict1, dict2); | |
Assert.False(dict1 == dict2); | |
Assert.True(dict1 != dict2); | |
Assert.False(dict1.Equals(dict2)); | |
} | |
[Fact] | |
public void RecordsUseDeepEquality() | |
{ | |
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality(); | |
var array1Copy = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality(); | |
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality(); | |
var dict1Copy = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality(); | |
var model1 = new TestModel(array1, dict1); | |
var model1Copy = new TestModel(array1Copy, dict1Copy); | |
Assert.Equal(model1, model1Copy); | |
Assert.True(model1 == model1Copy); | |
Assert.False(model1 != model1Copy); | |
Assert.True(model1.Equals(model1Copy)); | |
} | |
[Fact] | |
public void Records_ThatUseDeepEquality_CanSerialize() | |
{ | |
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality(); | |
var dict1 = new Dictionary<string, string> { { "Key1", "1" } }.ToImmutableDictionaryWithDeepEquality(); | |
var model1 = new TestModel(array1, dict1); | |
var json = System.Text.Json.JsonSerializer.Serialize(model1); | |
Assert.Equal("{\"Ints\":[1,2,3],\"Dictionary\":[{\"Key\":\"Key1\",\"Value\":\"1\"}]}", json); | |
} | |
[Fact] | |
public void Records_ThatUseDeepEquality_CanDeserialize() | |
{ | |
var json = "{\"Ints\":[1,2,3],\"Dictionary\":[{\"Key\":\"Key1\",\"Value\":\"1\"}]}"; | |
var modelFromJson = System.Text.Json.JsonSerializer.Deserialize<TestModel>(json); | |
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality(); | |
var dict1 = new Dictionary<string, string> { { "Key1", "1" } }.ToImmutableDictionaryWithDeepEquality(); | |
var model1 = new TestModel(array1, dict1); | |
Assert.Equal(model1, modelFromJson); | |
} | |
private sealed record TestModel( | |
ImmutableArrayWithDeepEquality<int> Ints, | |
ImmutableDictionaryWithDeepEquality<string, string> Dictionary); | |
} | |
} |
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
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text.Json.Serialization; | |
namespace System.Collections.Immutable | |
{ | |
[JsonConverter(typeof(JsonConverterForImmutableDictionaryWithDeepEqualityFactory))] | |
public class ImmutableDictionaryWithDeepEquality<TKey, TValue> : IEquatable<ImmutableDictionaryWithDeepEquality<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable, IReadOnlyCollection<KeyValuePair<TKey, TValue>> where TKey : notnull | |
{ | |
private readonly ImmutableDictionary<TKey, TValue> _dictionary; | |
public ImmutableDictionaryWithDeepEquality(ImmutableDictionary<TKey, TValue> dictionary) => _dictionary = dictionary; | |
#region ImmutableArray Implementation | |
public TValue this[TKey index] => _dictionary[index]; | |
public int Count => _dictionary.Count; | |
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Add(TKey key, TValue value) => _dictionary.Add(key, value).WithDeepEquality(); | |
public ImmutableDictionaryWithDeepEquality<TKey, TValue> AddRange(IEnumerable<KeyValuePair<TKey, TValue>> pairs) => _dictionary.AddRange(pairs).WithDeepEquality(); | |
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Clear() => _dictionary.Clear().WithDeepEquality(); | |
public ImmutableDictionary<TKey, TValue>.Enumerator GetEnumerator() => _dictionary.GetEnumerator(); | |
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Remove(TKey key) => _dictionary.Remove(key).WithDeepEquality(); | |
public ImmutableDictionaryWithDeepEquality<TKey, TValue> RemoveRange(IEnumerable<TKey> keys) => _dictionary.RemoveRange(keys).WithDeepEquality(); | |
public ImmutableDictionaryWithDeepEquality<TKey, TValue> SetItem(TKey key, TValue value) => _dictionary.SetItem(key, value).WithDeepEquality(); | |
public bool IsEmpty => _dictionary.IsEmpty; | |
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> Empty = new(ImmutableDictionary<TKey, TValue>.Empty); | |
#endregion | |
#region IEnumerable | |
IEnumerator IEnumerable.GetEnumerator() => (_dictionary as IEnumerable).GetEnumerator(); | |
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => (_dictionary as IEnumerable<KeyValuePair<TKey, TValue>>).GetEnumerator(); | |
#endregion | |
#region IEquatable | |
public bool Equals(ImmutableDictionaryWithDeepEquality<TKey, TValue> other) => _dictionary.SequenceEqual(other); | |
public override bool Equals(object obj) => obj is ImmutableDictionaryWithDeepEquality<TKey, TValue> other && Equals(other); | |
public static bool operator ==(ImmutableDictionaryWithDeepEquality<TKey, TValue>? left, ImmutableDictionaryWithDeepEquality<TKey, TValue>? right) => left is null ? right is null : right is not null && left.Equals(right); | |
public static bool operator !=(ImmutableDictionaryWithDeepEquality<TKey, TValue>? left, ImmutableDictionaryWithDeepEquality<TKey, TValue>? right) => !(left == right); | |
public override int GetHashCode() | |
{ | |
unchecked | |
{ | |
return _dictionary.Aggregate(19, (h, i) => h * 19 + i.Key.GetHashCode() + (i.Value?.GetHashCode() ?? 0)); | |
} | |
} | |
#endregion | |
} | |
public static class ImmutableDictionaryWithDeepEqualityEx | |
{ | |
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> WithDeepEquality<TKey, TValue>(this ImmutableDictionary<TKey, TValue> dictionary) where TKey : notnull => new(dictionary); | |
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> ToImmutableDictionaryWithDeepEquality<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> list) where TKey : notnull => new(list.ToImmutableDictionary()); | |
} | |
} |
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
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
namespace System.Collections.Immutable | |
{ | |
public class JsonConverterForImmutableArrayWithDeepEqualityFactory : JsonConverterFactory | |
{ | |
public override bool CanConvert(Type typeToConvert) | |
=> typeToConvert.IsGenericType | |
&& typeToConvert.GetGenericTypeDefinition() == typeof(ImmutableArrayWithDeepEquality<>); | |
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) | |
{ | |
var elementType = typeToConvert.GetGenericArguments()[0]; | |
var arrayType = typeof(JsonConverterForImmutableArrayWithDeepEquality<>); | |
var converter = (JsonConverter)Activator.CreateInstance( | |
arrayType.MakeGenericType(elementType), | |
BindingFlags.Instance | BindingFlags.Public, | |
binder: null, | |
args: null, | |
culture: null)!; | |
return converter; | |
} | |
private class JsonConverterForImmutableArrayWithDeepEquality<T> : JsonConverter<ImmutableArrayWithDeepEquality<T>> | |
{ | |
public override ImmutableArrayWithDeepEquality<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |
{ | |
if (reader.TokenType != JsonTokenType.StartArray) | |
{ | |
throw new JsonException(); | |
} | |
reader.Read(); | |
List<T> elements = new(); | |
while (reader.TokenType != JsonTokenType.EndArray) | |
{ | |
var value = JsonSerializer.Deserialize<T>(ref reader, options); | |
if (value is not null) | |
{ | |
elements.Add(value); | |
} | |
reader.Read(); | |
} | |
return elements.ToImmutableArrayWithDeepEquality(); | |
} | |
public override void Write(Utf8JsonWriter writer, ImmutableArrayWithDeepEquality<T> value, JsonSerializerOptions options) | |
{ | |
JsonSerializer.Serialize(writer, value.AsEnumerable(), options); | |
} | |
} | |
} | |
} |
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
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
namespace System.Collections.Immutable | |
{ | |
public class JsonConverterForImmutableDictionaryWithDeepEqualityFactory : JsonConverterFactory | |
{ | |
public override bool CanConvert(Type typeToConvert) => | |
typeToConvert.IsGenericType | |
&& typeToConvert.GetGenericTypeDefinition() == typeof(ImmutableDictionaryWithDeepEquality<,>); | |
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) | |
{ | |
var keyType = typeToConvert.GetGenericArguments()[0]; | |
var valueType = typeToConvert.GetGenericArguments()[1]; | |
var dictionaryType = typeof(JsonConverterForImmutableDictionaryWithDeepEquality<,>); | |
var converter = (JsonConverter)Activator.CreateInstance( | |
dictionaryType.MakeGenericType(keyType, valueType), | |
BindingFlags.Instance | BindingFlags.Public, | |
binder: null, | |
args: null, | |
culture: null)!; | |
return converter; | |
} | |
private class JsonConverterForImmutableDictionaryWithDeepEquality<TKey, TValue> : JsonConverter<ImmutableDictionaryWithDeepEquality<TKey, TValue>> where TKey : notnull | |
{ | |
public override ImmutableDictionaryWithDeepEquality<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |
{ | |
if (reader.TokenType != JsonTokenType.StartArray) | |
{ | |
throw new JsonException(); | |
} | |
reader.Read(); | |
Dictionary<TKey, TValue> elements = new(); | |
while (reader.TokenType != JsonTokenType.EndArray) | |
{ | |
var value = JsonSerializer.Deserialize<KeyValuePair<TKey, TValue>>(ref reader, options); | |
elements.Add(value.Key, value.Value); | |
reader.Read(); | |
} | |
return elements.ToImmutableDictionaryWithDeepEquality(); | |
} | |
public override void Write(Utf8JsonWriter writer, ImmutableDictionaryWithDeepEquality<TKey, TValue> value, JsonSerializerOptions options) | |
{ | |
JsonSerializer.Serialize(writer, value.AsEnumerable(), options); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment