This library is a work in progress. Existing functionality is pretty much done, but public interfaces may change.
SimpleValueObjects
is a toolkit that let's you implement Value Objects easily. It's a .NET Standard library, so you can use it both on .NET Core and good ol' .NET Framework.
Let's say you want to create a Position
class in your system. A perfect candidate for a Value Object!
First, install SimpleValueObjects
:
PM> Install-Package SimpleValueObjects
Implement your Value Object:
using SimpleValueObjects;
public class Position : AutoValueObject<Position>
{
public int X { get; }
public int Y { get; }
public Position(int x, int y)
{
X = x;
Y = y;
}
}
Enjoy hassle-free equality comparison:
var first = new Position(5, 5);
var second = new Position(5, 5);
Console.WriteLine(first == second); // True
Console.WriteLine(first != second); // False
Console.WriteLine(first.Equals(second)); // True
Also, since both equality comparison and generating hash codes are taken care of, you can use your Value Objects in all sorts of standard collections and LINQ methods:
private HashSet<Position> _visitedLocations;
public bool WasLocationVisited(Position position)
{
return _visitedLocations.Contains(position);
}
public IEnumerable<Position> NotVisitedLocations(IEnumerable<Position> positions)
{
return positions.Except(_visitedLocations);
}
A Value Object is an object, that:
- represents a value,
- is immutable and
- whose equality is based on value, rather than identity or reference.
This seemingly simple pattern has a wide range of applications. Actually, if you think about it, you're probably using ones on a daily basis - think of string
or DateTime
. Other popular examples are amounts of money, date ranges or geographical coordinates.
Domain Driven Design promotes using Value Objects for representation of domain concepts, e. g. user names, product prices or ZIP codes.
Value Object seem pretty useful! However, .NET doesn't make implementation of proper Value Objects easy, with all its operator overloading, non-generic methods overriding, types handling, null handling and hash code generation. In other words, creating a solid Value Object by hand is tricky and can be quite a hassle, not to mention code and test duplication it produces.
This library is meant to make implementation of Value Objects as simple as possible.
This library consists of a few base classes you can use for different scenarios. They're split into two groups:
- Value Objects, which are equality compared,
- Comparable Value Object, which are order compared, i.e. they can be lesser, equal or greater than one another.
Using Value Object base classes will provide following things:
- overloaded
==
and!=
operators, - overridden
Object.Equals
, IEquatable<T>.Equals
implementation,- null handling: no value is equal to null and two nulls are always equal,
- type handling: different types are never equal.
In most cases, a Value Object consists of a few fields. Obviously, we consider such objects equal, when their fields' values are also equal.
AutoValueObject
will automatically implement equality and hash code generation using reflection.
public class Money : AutoValueObject<Money>
{
public Currency Currency { get; }
public decimal Amount { get; }
public Money(Currency currency, decimal amount)
{
Currency = currency;
Amount = amount;
if (amount < 0)
{
throw new ArgumentException(
$"Money cannot have negative amount, but got {amount}.");
}
}
public override string ToString() => $"{Amount} {Currency}";
}
Remarks:
AutoValueObject
uses reflection to get fields' values, which means it might be slow at times. I believe in most cases that is not a huge problem, however if you use your Value Object extensively you might want to useValueObject
, which let's you implement equality comparison by hand.- Also,
AutoValueObject
doesn't perform deep comparison. It means, that if you want your Value Object to be more structured, you should either compose it from other Value Objects, or extendValueObject
instead and implement equality comparison by hand.
Sometimes you'll just want to use some more common value, like string
or int
and give it additional context.
For instance, user name in a system can be just a string, but with limited length and consisting only of alphanumeric characters.
In that case you should use WrapperValueObject
, which wraps exactly one value and uses it for equality comparison and hash code generation.
public class UserName : WrapperValueObject<UserName, string>
{
public UserName(string userName)
: base(userName)
{
if (userName == null)
{
throw new ArgumentException(
"User name cannot be null.");
}
if (!_userNamePattern.IsMatch(userName))
{
throw new ArgumentException(
$"User name should match pattern {_userNamePattern}, " +
$"but found '{userName}' instead.");
}
}
private readonly Regex _userNamePattern = new Regex("[a-z0-9-]{5,25}");
}
Remarks:
WrapperValueObject
doesn't perform deep comparison. It means, that wrapped type should also be a value.
When you want to implement equality comparison logic by yourself, you can use ValueObject
.
You have to implement generating hash code yourself (see generating hash codes section below).
public class IntRange : ValueObject<IntRange>
{
public int From { get; }
public int To { get; }
public IntRange(int from, int to)
{
From = from;
To = to;
if (From > To)
{
throw new InvalidOperationException(
$"From cannot be greater than To in IntRange, but got: {From}-{To}.");
}
}
protected override bool EqualsNotNull(IntRange notNullOther)
{
return From == notNullOther.From && To == notNullOther.To;
}
protected override int GenerateHashCode()
{
return HashCodeCalculator.CalculateFromValues(From, To);
}
}
Sometimes we want values that not only can be determined equal, but also can be placed in a certain order. Most commonly used cases would be an int
or a DateTime
. We can certainly say, that values of such types can be lesser, equal or greater that another values.
More custom examples would be: temperature, user rating or a month. Again, they can be put in an order: 5 star rating is greater than just 3 stars, or February comes after January.
Using Comparable Value Object base classes will guarantee following things:
- equivalent order comparison and equality comparison,
- overloaded
<
,<=
,==
,!=
,>
and>=
operators, - overridden
Object.Equals
, IEquatable<T>
,IComparable
andIComparable<T>
implementations,- null handling: every value is greater than null, no value is equal to null and two nulls are always equal,
- type handling: different types are never equal and comparing objects of different types will throw an exception.
Like with WrapperValueObject
, sometimes you'll want to wrap a simpler value to give it additional context. You can do that, if wrapped type implements IComparable<T>
(where T
is itself), which then will be used for comparison.
public class MovieRating : WrapperComparableObject<MovieRating, int>
{
public MovieRating(int stars)
: base(stars)
{
if (stars < 1 || stars > 5)
{
throw new ArgumentException(
$"UserRating should be between 1 and 5 stars, but got {stars} stars.");
}
}
public static readonly MovieRating Lowest = new MovieRating(1);
public static readonly MovieRating Highest = new MovieRating(5);
public override string ToString() => new string('★', count: Value);
}
With ComparableObject
you only implement comparison once, and all comparison and equality comparison logic are handled consistently.
You have to implement generating hash code yourself (see generating hash codes section below).
public class YearMonth : ComparableObject<YearMonth>
{
public int Year { get; }
public Month Month { get; }
public YearMonth(int year, Month month)
{
Year = year;
Month = month;
if (!Enum.IsDefined(typeof(Month), month))
{
throw new ArgumentException($"Month {month} is not valid.");
}
}
public YearMonth Next()
{
return Month == Month.December
? new YearMonth(Year + 1, Month.January)
: new YearMonth(Year, Month + 1);
}
protected override int CompareToNotNull(YearMonth notNullOther)
{
return Year != notNullOther.Year
? Year - notNullOther.Year
: Month - notNullOther.Month;
}
protected override int GenerateHashCode() => HashCodeCalculator.CalculateFromValues(Year, Month);
public override string ToString() => $"{Month} {Year}";
}
Sometimes you'll need to generate a hash code by hand, e.g. when using ValueObject
or ComparableObject
. If you just want to generate a hash from a bunch of different values, you can use HashCodeCalculator.CalculateFromValues
method:
protected override int GenerateHashCode() => HashCodeCalculator.CalculateFromValues(Year, Month);
Remember: when two Value Objects are equal, their hash codes should also be equal.
If you're curious, you can check some official sources.
Some interesting resouces related to Value Objects: