Skip to content

A toolkit for simple creation of various Value Objects in .NET (work in progress).

License

Notifications You must be signed in to change notification settings

redss/SimpleValueObjects

Repository files navigation

SimpleValueObjects

AppVeyor NuGet

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.

Quickstart

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);
}

Introduction to Value Objects

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.

Using SimpleValueObjects

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.

Value Objects

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.

AutoValueObject

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 use ValueObject, 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 extend ValueObject instead and implement equality comparison by hand.

WrapperValueObject

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.

ValueObject

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);
    }
}

Comparable Value Objects

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 and IComparable<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.

WrapperComparableObject

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);
}

ComparableObject

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}";
}

Generating hash codes

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.

Resources

Some interesting resouces related to Value Objects:

About

A toolkit for simple creation of various Value Objects in .NET (work in progress).

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published