-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Background
As a result of #1, I added some logic that changed how hash code and equality checks were done for types that implement IEnumerable. They are as follows:
- If the type for a given property implements
ISet<T>, we perform an order agnostic comparison of all items in the collection. - If the type otherwise implements
IEnumerableand is not a string, we perform an order-specific comparison of all items in the collection. - Finally, if the type is
Stringuse the default String comparison ofOrdinal.
While these are reasonable defaults, there is no guarantee a user wishes this behavior to happen all of the time. For example, a property of type SortedSet<T> may care about order, despite implementing ISet<T>, or a property may be of a type that implements IEnumerable, but the instead should use the standard equality and hash code generation implementations. We need to support these one-off cases to override the default behavior in ImmutableBase<TImmutable>.
Task
Update ImmutableBase<TImmutable> to look for and respect an [EnumerableComparison] like attribute that can be applied to types of IEnumerable, letting the comparison logic know how that property is meant to have its hash code and equality calculated. For example, it could look like this:
public class CustomEnumerable : ImmutableBase<CustomEnumerable>, IEnumerable {
public String Key { get; }
public IEnumerable Values { get; }
public CustomEnumerable(String key, IEnumerable values) {
if (values == null) {
throw new ArgumentNullException(nameof(values));
}
this.Key = key;
this.Values = values;
}
public IEnumerator GetEnumerator() {
return this.Values.GetEnumerator();
}
}
public class MyImmutable : ImmutableBase<MyImmutable> {
[EnumerableComparisonAttribute(EnumerableComparison.AsSet)]
public IEnumerable<Guid> Ids { get; }
[EnumerableComparisonAttribute(EnumerableComparison.Default)]
public CustomEnumerable Custom { get; }
public MyImmutable(ISet<Guid> ids, CustomEnumerable custom) {
this.Ids = ids;
this.Custom = custom;
}
}
public class MyImmutableTests {
[Fact]
public void InequalityForIds() {
// Arrange
var idsOne = new [] { Guid.NewGuid(), Guid.NewGuid() };
var idsTwo = new [] { idsOne[1], idsOne[0] };
var immutableOne = new MyImmutable(idsOne, null);
var immutableTwo = new MyImmutable(idsTwo, null);
// Act
var areEqual = immutableOne.Equals(immutableTwo);
// Assert
Assert.True(
areEqual,
"This would be 'false' with current logic, " +
"since the default comparison for IEnumerable considers order."
);
}
[Fact]
public void InequalityForCustomEnumerable() {
// Arrange
var customOne = new CustomEnumerable("one", new [] { "foo", "bar" });
var customTwo = new CustomEnumerable("two", new [] { "foo", "bar" });
var immutableOne = new MyImmutable(new [] { Guid.NewGuid() }, customOne);
var immutableTwo = new MyImmutable(new [] { Guid.NewGuid() }, customTwo);
// Act
var areEqual = immutableOne.Equals(immutableTwo);
// Assert
Assert.False(
areEqual,
"This would be 'true' with current logic, " +
"since CustomEnumerable implements IEnumerable."
);
}
}Note
If the application of an [EnumerableComparison] attribute changes an IEnumerable implementation to instead use the standard hash code and equality logic, the ToString() logic referenced in #13 should respect this as well.