You’re not limited to using Value Objects as entity property types. They have a lot of valuable uses as method return values. In fact you can increase your software suppleness by using Value Objects as method return types. The purpose of the class doesn’t matter either. Whether it serves as a repository, domain service, infrastructure service is irrelevant.

If you haven’t already go back and read Value Objects: The Basics.

Let’s dive right in to our example.

Returning a Value Object from Repositories

For the reason that sometimes repositories expose methods that return data outside entity form they serve as a real world example.

Let’s say your repository needs to execute an ad-hoc query that queries customers and returns the counts grouped by birth year. Instead of returning an IDictionary<TGroupingKey, long> where in this case TGroupingKey will be a string representing the birth year you could instead return an IReadOnlyCollection<BirthCount>.

public interface CustomerRepository : IRepository<Customer>
{
    IReadOnlyCollection<BirthCount> BirthCounts();
}

BirthCount returned by the domain repository interface could look something like this.

public class BirthCount : ValueObjectBase<BirthCount>
{
    public BirthCount(int birthYear, int count)
    {
        // initialize value object
    }

    public int BirthYear { get; private set; }

    public int Count { get; private set; }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return BirthYear;
        yield return Count;
    }
}

Because we used a Value Object here there isn’t guess work as to what TGroupingKey represents. Does it represent the Year, the Month or a string concatenation of Year and Month and if it represents a string concatenation of Year and Month? What’s the format of the TGroupingKeystring in case we need to parse and extract the components?

Certainly without Value Objects we rely too much on the consumer knowing how the underlying query logic implementation. For example how to parse TGroupingKey when needed.

Here are some advantages of using a Value Object for this scenario.

  • It’s clear to the repository interface consumer what the underlying data represents in the model.
  • Repository return value is parsed by the repository internals and then stored into properties of the Value Object.

Testing the Suppleness

Time to test the software for suppleness by handling the scenario mentioned previously. The business requirements change and now we need to group by birth year and month.

With the power of Value Objects we don’t have to worry about relying on the repository interface consumer to parse the string format of TGroupingKey.

public class BirthCount : ValueObjectBase<BirthCount>
{
    public BirthCount(int birthYear, string birthMonth, int count)
    {
        // initialize value object
    }

    public int BirthYear { get; private set; }

    public string BirthMonth { get; private set; }

    public int Count { get; private set; }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return BirthYear;
        yield return BirthMonth;
        yield return Count;
    }
}

That was pretty easy! We simply added a BirthMonth property for the new composite grouping key then updated the BirthCount constructor call made by the repository method.

Without using Value Objects we would’ve unknowingly caused regression bugs. Consumers of our repository sometimes need to parse the return data, in this case TGroupingKey. Because birth year and month are separate Value Object properties consumers don’t need to parse any data. Therefore, existing code usages can continue referencing BirthYear without worrying about the addition of BirthMonth.

Wrap Up

So there you have it. I hoped you enjoyed this and learned a little about what Value Objects bring to the table with method return values. Spend a little more time in the beginning to save a lot of time in the long run. Use Value Objects as method return values when ever possible.