.NET Value Type (struct) as a DDD Value Object
I got a suggestion recently about using .NET structs to represent DDD Value Objects to which I repeated what I’ve been saying and writing for several years now: structs are not a good choice for DDD Value Objects. But then I realized that I never actually dove into the details of why it is so.
So here it is, the blog post where we’ll talk about using .NET Value Types (structs) as DDD Value Objects and what effect it has on the domain model, performance, and mapping the model to the database using ORMs.
ORM support
The first point to consider when using structs is ORM support. I often say that ORMs don’t play well with structs but ORMs actually do support them to some extent. With NHibernate, you can define a custom struct and use it as a Component (Value Object in NHibernate’s terminology). Dapper also allows you to do that: you can use structs to represent data returned from the database.
So ORM support for structs is OK, but only if you don’t define your entities as structs. That is not possible due to the fundamental limitations of .NET value types. You can’t inherit from a struct whereas "big" ORMs rely on the ability to create runtime proxy classes on top of your entities in order to track changes in them. Not that you want to do that anyway. Structs are supposed to be immutable which contradicts the inherently mutable nature of your entities.
And what about Entity Framework? EF6 doesn’t support using structs as complex types, but EF Core 2.0 does. At least it should, I haven’t actually checked it yet.
Equality comparison
Unlike reference types, .NET structs implement structural equality instead of reference equality. For example, this code returns true:
public struct Email
{
public string Value { get; }
public bool IsConfirmed { get; }
public Email(string value, bool isConfirmed)
{
Value = value;
IsConfirmed = isConfirmed;
}
}
Email email1 = new Email("[email protected]", true);
Email email2 = new Email("[email protected]", true);
bool isEqual = email1.Equals(email2); // true
So, does it mean that we get the Value Object behavior out of the box, for free? At least when it comes to equality comparison?
It does but this perk comes at a price. And that is performance. The default implementation of Equals() and GetHashCode() in .NET’s ValueType uses reflection to retrieve the list of fields in the type and thus renders this implementation completely inefficient.
Note that ValueType.Equals() performs well when the type consists of primitive values only (which should also be Value Types); it uses byte-by-byte comparison in this case. But in cases when your struct includes a reference type - and string is one of them - it falls back to using reflection to compare the instances field-to-field.
So, to get rid of the inefficiency, you will need to define your own Equals() and GetHashCode(), as well as implement the IEquatable interface to avoid unnecessary boxing and unboxing:
public struct Email : IEquatable<Email>
{
public string Value { get; }
public bool IsConfirmed { get; }
public Email(string value, bool isConfirmed)
{
Value = value;
IsConfirmed = isConfirmed;
}
public bool Equals(Email other)
{
return Value == other.Value && IsConfirmed == other.IsConfirmed;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
return false;
return obj is Email && Equals((Email)obj);
}
public override int GetHashCode()
{
unchecked
{
return ((Value != null ? Value.GetHashCode() : 0) * 397) ^ IsConfirmed.GetHashCode();
}
}
public static bool operator ==(Email a, Email b)
{
return a.Equals(b);
}
public static bool operator !=(Email a, Email b)
{
return !(a == b);
}
}
Note that along with Equals()
and GetHashCode()
, you also need to define custom equality operators (==
and !=
). Otherwise, code like this:
Email email1 = new Email("[email protected]", true);
Email email2 = new Email("[email protected]", true);
bool isEqual = email1 == email2;
will fall back to the default inefficient implementation.
This works fine but poses another problem: duplication. You will need to repeat the equality operators and part of the Equals method in each and every value object you define and won’t be able to factor common bits out because structs don’t support inheritance. Which is not too bad but still unpleasant.
Having that said, if you don’t care about the performance of your equality members much (which most enterprise developers shouldn’t), it’s fine to rely on the default equality comparison implementation. No need to overcomplicate your code if you don’t have to.
Issues with encapsulation
So far so good, right? Or at least not that bad. Let’s say that you use NHibernate or EF Core 2.0 and so your ORM does support using structs as Value Objects. Let’s also say that you don’t care that much about equality members performance. It means you can rely on the default ValueType’s equality implementation and not duplicate the custom code across different Value Objects.
Doesn’t it mean you can safely use .NET Value Types as your Value Objects? Unfortunately, it doesn’t.
Another issue with structs is that you cannot hide the default parameterless constructor from the eyes of the value object’s clients, and you also can’t override it with your own implementation.
So, along with this code:
Email email = new Email("[email protected]", true);
the client can also write this:
Email email = new Email();
and you can’t do anything about it.
Which means that even if you have some invariants associated with your value object (and you do in almost all cases), you cannot enforce them. The client code will always be able to get around them by calling the default parameterless constructor.
This is a deal breaker if you want to build a rich, highly encapsulated domain model. You don’t ever want to leave your clients a chance to violate the domain model’s invariants (not without raising an exception). And so if you want to maintain proper encapsulation, the only option you have is to use reference types for your Value Objects.
By the way, CLR does support defining parameterless constructors for structs but even when you do that, they don’t get called in scenarios when they are not invoked explicitly, like so:
Email[] emails = new Email[10];
The behavior here is akin to what deserializers do when they instantiate an object using FormatterServices
, you are getting an instance with all fields set to default values, regardless of whether you have a constructor defined:
FormatterServices.GetUninitializedObject(typeof(Customer));
To avoid confusion, C# designers just prohibited defining your own default constructor on a struct as it wouldn’t be called in such scenarios anyway.
This could be changed in a future C# version. It’s quite unlikely, though, as it would require the .NET team to reconsider some fundamental assumptions related to .NET Value Types.
Side note
There are quite a few enhancements EF Core made over the last year. It might be time for a new EF vs NHibernate comparison from a DDD standpoint as this one covers EF6 only. Let me know if that’s something you would be interested in.
I also get a lot of comments on my Pluralsight courses asking why I’m not using EF. I’m thinking about creating a course showcasing 3 different approaches to building a rich domain model: plain EF Core 2.0, EF Core 2.0 + persistence (data) model, and NHibernate. This will show the differences in ORMs when it comes to encapsulation, and will also show the pros and cons of using a dedicated data model. The content would be similar to what I did in my recent course but with the focus on ORMs instead. Let me know if that is something you would find interesting.
Summary
OK, let’s summarize:
-
NHibernate and EF Core 2.0 support structs as Value Objects (using their Component / Complex Type features) but EF6 doesn’t.
-
If you need performance, you have to define your own equality members in each struct and cannot factor any common logic out because structs don’t support inheritance.
-
You have to forgo encapsulation when using structs as they don’t allow you to hide or redefine the default parameterless constructor.
This makes .NET Value Types a bad choice when it comes to working with DDD Value Objects.
Related links
- ← New course: Refactoring from Anemic Domain Model Towards a Rich One
- NHibernate 5: async IO bound operations support →
Subscribe
Comments
comments powered by Disqus