.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:

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:

Note that along with Equals() and GetHashCode(), you also need to define custom equality operators (== and !=). Otherwise, code like this:

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:

the client can also write this:

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:

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:

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

Share




  • Muhammad Haggag

    Hello, Vladimir.

    I’m definitely interested in your insights on the 3 approaches to DDD with ORMs with and without persistence models. I’m a fan of all your works; you’ve helped and continue to help me wrap my head around DDD as I put it to practice. Even a series of blog posts highlighting what you think are the main takeaways would be great, and likely much less time-consuming for you compared to a course 🙂

    Best Regards

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Thanks 🙂 Will try to make it happen, would be a good driving factor for me to finally go through the ins and outs of EF Core.

    • Joseph N. Musser II

      Seconded.

    • http://www.duanewingett.info Dibley

      “Thirded” – I’d also like to see such a course on PS.

  • Sascha Bendinger

    Hi Vladimir,

    as always I get great insight when reading your posts, well done!

  • http://marianciortea.com/ Marian Ciortea

    Hi Vladimir! Excellent comparison between value type and DDD value object. I would be really interested in EF Core implementation of rich domain model because EF is widely used and I personally use it. Thanks and have a good day!

    • Paul Van Bladel

      +1 from me. I think, in the end, EF Core has a lot of potential.