Skip to content

Instantly share code, notes, and snippets.

@ryanholden8
Last active November 4, 2021 14:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanholden8/5602a6cc6decebb5bb35e21aa37efa67 to your computer and use it in GitHub Desktop.
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)
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());
}
}
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);
}
}
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());
}
}
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);
}
}
}
}
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