invio / Invio.Immutable

C# Library used to ease immutable class creation and data management
MIT License
2 stars 0 forks source link

Add [EnumerableComparison]-like annotation for IEnumerable properties #14

Open carusology opened 7 years ago

carusology commented 7 years ago

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:

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.