Representing a collection as a Value Object

I was reviewing the list of topic ideas lately and found this question in the discussion to my DDD in Practice Pluralsight course. While I answered it - somewhat briefly - in the discussion thread, I think it’s worth a separate detailed blog post. The question itself goes like this: "Can you have a collection of Value Objects abstracted as a Value Object itself?" Or, in other words, can you represent a collection as a Value Object?

Collection as a Value Object

In the world of DDD, there’s a well-known guideline that you should prefer Value Objects over Entities where possible. If you see that a concept in your domain model doesn’t have its own identity, choose to treat that concept as a Value Object. This approach has a lot of benefits, the most important of which is that Value Objects are much easier to work with. Check out this post to read more about the subject: Entity vs Value Object: the ultimate list of differences.

It’s quite easy to follow this guideline when you deal with single items. For example, if you want to introduce value object City which can be turned on and off for some reason, you can do it like this:

public sealed class City : ValueObject<City>
{
    public string Name { get; }
    public bool IsEnabled { get; }
 
    public City(string name, bool isEnabled)
    {
        Name = name;
        IsEnabled = isEnabled;
    }
 
    protected override bool EqualsCore(City other)
    {
        return Name == other.Name && IsEnabled == other.IsEnabled;
    }
 
    protected override int GetHashCodeCore()
    {
        return Name.GetHashCode() ^ IsEnabled.GetHashCode();
    }
}

But what to do if you need to have a collection of such cities? Can the collection itself also be represented as a separate Value Object? And how should we map it to the database in this case?

The common way to deal with collections is to introduce a 1-to-many relation, like this:

public class User : Entity
{
    public List<City> Cities { get; set; }
}

The problem, however, is that in order to do that, we need to also create a 1-to-many relationship in the database. In other words, introduce a separate table for cities and bind them together with users using a foreign key constraint:

A common way of representing a collection
A common way of representing a collection

That, in turn, makes us unable to treat City as a value object. This concept now has its own identifier which makes it effectively an entity. The problem with the common way of working with collections is that we inevitably have to represent items in them as entities if we want to persist those items in a relational data store.

We have to do that even if the items themselves are not entities per se. It becomes quite annoying at times as the number of entities in the model grows, making it more complex than necessary. How are we supposed to follow the guideline in such situation?

Luckily, there is a way. To overcome the problem, you need to introduce a separate class for the whole collection. Here’s how you can do that:

public class CityList : ValueObject<CityList>, IEnumerable<City>
{
    private List<City> _cities { get; }
 
    public CityList(IEnumerable<City> cities)
    {
        _cities = cities.ToList();
    }
 
    protected override bool EqualsCore(CityList other)
    {
        return _cities
            .OrderBy(x => x.Name)
            .SequenceEqual(other._cities.OrderBy(x => x.Name));
    }
 
    protected override int GetHashCodeCore()
    {
        return _cities.Count;
    }
 
    public IEnumerator<City> GetEnumerator()
    {
        return _cities.GetEnumerator();
    }
 
    IEnumeratorIEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Note that for CityList, the same rules apply as for any other Value Object. First of all, we need to compare instances of such a list using structural equality. And we do: the EqualsCore method in this example uses SequenceEqual. It goes through each item in two collections and makes sure they match.

CityList must also be immutable. It means that if you want to change an element in it or add a new one, you need to return a new list as a result of that operation:

public CityList AddCity(string name, bool isEnabled)
{
    List<City> cities = _cities.ToList();
    cities.Add(new City(name, isEnabled));
 
    return new CityList(cities);
}

Finally, the collection should not hold an identity of any kind. The easiest way to ensure that is to store the whole list of cities in a single field of the User table. A simple serialization mechanism that converts the list to and from a string would suffice here:

public static explicit operator CityList(string cityList)
{
    List<City> cities = cityList.Split(';')
        .Select(x => (City)x)
        .ToList();
 
    return new CityList(cities);
}
 
public static implicit operator string(CityList cityList)
{
    return string.Join(";", cityList.Select(x => (string)x));
}

And, to actually persist a list to the data store along with other users' data, you need to transform it to a string behind the scene, like this:

public class User : Entity
{
    private string _cities = string.Empty;
    public virtual CityListCities
    {
        get { return (CityList)_cities; }
        set { _cities = value; }
    }
}

That string then can be used by ORM. The resulting User row in the database would like the following in this case:

UserID: 1,
CityList: "City1|true;City2|false;"

Collection as a Value Object: drawbacks

This approach helps stick to the guideline regarding preferring Value Objects over Entities and thus keep the domain model simpler. However, there are some shortcomings to this implementation.

The first one is a corollary from the use of the serialization format that helps us persist the list to the database. That is, you would not be able to perform an effective search in such a de-normalized data. That might be just fine if you don’t need to, but if you do, the approach described above wouldn’t fit your situation.

Secondly, if the number of items in the collection tends to be high, treating it as a Value Object wouldn’t be sufficient either. You need to decide where to draw the line yourself, that depends on performance requirements you’ve got to meet. But I would say that a hundred, maybe a couple hundred items is too many for most applications. Serializing them into a single string when persisting the entity would degrade the performance.

In the example above, if a user is about to have 200 cities, I would be reluctant to implement that list as a Value Object.

Finally, this approach is applicable only when you need to store a list of Value Objects, not a list of Entities. The latter would require you to keep references to them in the same manner we did previously:

ListOfEntities: "12;13;15"

And that is just a plain abuse of relational database principles. You wouldn’t be able to enforce referential integrity through the normal database constraints and you would not be able to use ORMs properly.

So, a collection can be a Value Object only when items in it themselves are Value Objects.

Having that said, the question still remains: what are you supposed to do if you do need to search among items in the collection or their number is too high? In such case, treating the items as Entities is the only viable option: you would need to create a separate table for them and deal with the identifier each of them will have after that.

But even in this case, there is a way to bring your code closer to the "Value Objects over Entities" guideline. I’ll write about how to do that in the next post.

Summary

Having a collection of items represented as a Value Object is possible. To do that, you need to:

  • Store the whole collection in a single database table field.

  • Come up with a serialization mechanism which would convert the collection from and to a string in order to save it in that field.

There are some drawbacks to that solution, so you need to carefully weigh the pros and cons of this approach. The drawbacks are:

  • You are unable to perform an effective search among individual items in the collection.

  • The approach may hit performance if you’ve got a lot of items in the collection.

  • It is applicable only when items in the collection themselves are also Value Objects.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus