Awesome
ByValue
This library helps to create ValueObjects with properly implemented equality behavior:
- Provides base
ValueObject
class. - Gives extension
ByValue()
for comparing collections with semantic of ValueObject (IReadOnlyCollection
,IReadOnlyDictionary
,IDictionary
andISet
are supported).
Example
public class MultilineAddress : ValueObject
{
public MultilineAddress(IReadOnlyCollection<string> addressLines, string city, string postalCode)
{
AddressLines = addressLines ?? throw new ArgumentNullException(nameof(addressLines));
City = city ?? throw new ArgumentNullException(nameof(city));
PostalCode = postalCode ?? throw new ArgumentNullException(nameof(postalCode));
if (addressLines.Count < 1 || addressLines.Count > 3)
throw new ArgumentOutOfRangeException(nameof(addressLines), addressLines, "Multiline address should have from 1 to 3 address lines");
}
public IReadOnlyCollection<string> AddressLines { get; }
public string City { get; }
public string PostalCode { get; }
// here you should return values, which will be used in Equals() and GetHashCode()
protected override IEnumerable<object> Reflect()
{
// by default collections compared with not strcit ordering
yield return AddressLines.ByValue(Ordering.Strict);
// you can transform object's properties when return them
yield return City.ToUpperInvariant();
yield return PostalCode;
}
}
SingleValueObject
Inherit from SingleValueObject
to boost performance when ValueObject has only one property:
public class UserId : SingleValueObject<int>
{
public UserId(int value)
: base(value)
{
if (value == 0)
throw new ArgumentOutOfRangeException(nameof(value));
}
public static explicit operator UserId(int value)
{
return new UserId(value);
}
public static implicit operator int(UserId userId)
{
return userId == null ? 0 : userId.Value;
}
public static implicit operator int? (UserId userId)
{
return userId == null ? (int?)null : userId.Value;
}
}
Custom EqualityComparer
When using ByValue()
to compare collections, elements will be compared using default EqualityComparer
. Sometimes this is not acceptable.
Here is AddressBook
which implements value object semantic:
public class AddressBook : ReadOnlyCollection<MultilineAddress>
{
public AddressBook(IList<MultilineAddress> list)
: base(list)
{
}
public override bool Equals(object obj)
{
if (obj is null)
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != GetType())
return false;
var other = obj as AddressBook;
var thisByValue = this.ByValue(); // not strict ordering
var otherByValue = other.ByValue();
return thisByValue.Equals(otherByValue);
}
public override int GetHashCode()
{
return Items.Count;
}
}
But you need address book which will not treat address with city "Foo" equal to address with city "fOO". Possible solution is to use custom EqualityComparer
for MultilineAddress
in derived EnhancedAddressBook
:
public class EnhancedAddressBook : AddressBook
{
public EnhancedAddressBook(IList<MultilineAddress> list)
: base(list)
{
}
public override bool Equals(object obj)
{
if (obj is null)
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != GetType())
return false;
var other = obj as EnhancedAddressBook;
var thisByValue = this.ByValue(x => x.UseComparer(EnhancedAddressComparer.Instance));
var otherByValue = other.ByValue(x => x.UseComparer(EnhancedAddressComparer.Instance));
return thisByValue.Equals(otherByValue);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
private class EnhancedAddressComparer : IEqualityComparer<MultilineAddress>
{
public static EnhancedAddressComparer Instance => new EnhancedAddressComparer();
public bool Equals(MultilineAddress x, MultilineAddress y)
{
if (ReferenceEquals(x, y))
return true;
if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
return false;
return x.AddressLines.ByValue(Ordering.Strict).Equals(y.AddressLines.ByValue(Ordering.Strict))
// do not ignore case of chars
&& x.City == y.City
&& x.PostalCode == y.PostalCode;
}
public int GetHashCode(MultilineAddress obj)
{
return 1;
}
}
}
More examples could be found in tests.