Domain Driven Design in C#: Immutable Value Objects

By Julie Lerman    |    December 09, 2020

In his book, Domain Driven Design (DDD), Eric Evans encourages the use of Value Objects in domain models – immutable types that are used as properties of entities. You can learn more about value objects and DDD in the Domain-Driven Design Fundamentals course which I co-authored with Steve Smith. Pluralsight’s development team has long been a fan of using the DDD approach to modeling, a leverage Value Objects throughout their domain layer. In a blog post from 2012, Keith Sparkjoy remarked that this made their C# code feel more and more functional in nature, which has lots of benefits. “Less mutable state tends to make things easier to test and tends to reduce bugs.”

Building immutability into your type is more than simply hiding the setters of properties. Paired with one of the most important facets of a value object – that its identity is tied to the composition of its values and not a random identity property – there are much sounder patterns for ensuring your value object is immutable. The keys to designing a value object are:

  1. It is immutable

  2. Its only use is as a property of one or more entities or even another value object

  3. It knows to use all of its properties when performing any type of equality check, including those which base their comparison on hash codes 

  4. Its logic should have no side effects on state outside of the value object

An Example Value Object: PersonFullName

Let’s look at a type that can be reused throughout a system, a type representing a person’s name. This can be used for subscribers, authors, contacts and more. I’ll call it PersonFullName and begin with a simple version then evolve it to a value object.

Let’s say Pluralsight engineers have defined a business rule that every individual should have a given name (often referred to as a first name) and a surname (aka last name).  My PersonFullName class first defines two private fields, _given and _surname. A Create factory method enforces that you can never create a PersonFullName without passing in these two name parts. That ensures that the name will never be in an invalid state. The factory then passes those onto the constructor which sets the field values. In this simple case, you could argue that the factory method is overkill. But it is here so you see an example of that implementation. Finally, I use expression body definitions (see Dániel Szabó’s blog post to learn more about these) on the properties to ensure the properties are read only. The properties only know how to return the values of the fields. Using expressions also allows me to nod to Keith’s admiration of their C# code leaning towards functional programming.

public class PersonFullName
        private string _given;
        private string _surname;

        public static PersonFullName Create(string given, string surname)
            return new PersonFullName (given, surname);
        private PersonFullName(string given, string surname)
            _given = given;
            _surname = surname;
        public string Given => _given;
        public string Surname => _surname;

Notice there is no identity property here. The name will never be tracked on its own. This class will only ever be used as a property of some other type.

Already the type is immutable -- once you’ve created the object, there is no way to modify it. If you spelled the person’s name wrong, you would just create a new instance.  Just as you would with a string. 

Here is the Author class that uses the value object.

public class Author
        private Guid _id;
        private PersonFullName _name;

        public Author(string given, string surname)
            _id = Guid.NewGuid();
            _name = PersonFullName.Create(given, surname);

        public Guid Id => _id;
        public PersonFullName Name=>_name;

 public void FixAuthorName(string given, string surname)
        { _name = PersonFullName.Create(given, surname); 

The Author’s constructor also requires that we supply the given and surname values and then uses them to create a PersonFullName object along with a newly generated Guid for its identity. If you need to change the name, you can call the FixAuthorName method to just recreate that name on the fly. DDD guides us to think about behaviors in our entities, not just setting and getting property values. FixAuthorName represents a behavior needed to solve a particular problem.

Just because PersonFullName is immutable, doesn’t mean the type can’t be smart. We often need other ways of viewing names and we can build that right into the PersonFullName type. For example, I want to be able to see the full name and alphabetize by surname. I’ve added a few more read-only properties, FullName and Reverse, again  using expression body definitions.

 public string FullName => _given + " " + _surname;
 public string Reverse => _surname+", "+ _given;

I could add this clever little method to allow us to create a new family member name with the same surname.

public PersonFullName FamilyMemberWithSameSurname(string newGivenName)
              => Create(newGivenName, _surname);

Making the Value Object Perform Proper Comparisons

PersonFullname is now an immutable type that has two values and no identifier. In order for this to be a value object, I still need to ensure that I can compare two PersonFullName types to determine equality. And I need the flexibility to do that using the Equals method inherited by every object in C# as well as with == or != operators. I also want to consider testing where I might be using the MSTest Assert.AreEqual or assertions from other frameworks.

Comparing two instances of this value object with Equals means comparing every one of the values in the objects. Some equality checks are done using the hashcodes of the object. This means adding a bunch more boilerplate code. There are few interesting ways to achieve this. 

Visual Studio’s Equals & GetHashCode Quick Action

One way is to leverage a Visual Studio Quick Action feature. One of the actions for a class declaration is to generate (i.e., override) Equals and GetHasCode.

generate equals and GetHashCode


This will ask what members to include. I only want to engage given and surname and will choose the properties, not the fields. One of the other options is to implement IEquatable which you want to do so that it will not only add the override for Equals, but also adds an Equals method for the specific type. The other important option is to generate operators. That will ensure you can use == and != in your code and correctly compare two PersonFullName objects.

The updated class now looks like this:

 public class PersonFullName : IEquatable<PersonFullName>
      . . . original ctor, properties and methods are still here . . .

        public override bool Equals(object obj)
            return Equals(obj as PersonFullName);

        public bool Equals(PersonFullName other)
            return other != null &&
                   Given == other.Given &&
                   Surname == other.Surname;

        public override int GetHashCode()
            return HashCode.Combine(Given, Surname);

        public static bool operator ==(PersonFullName left, PersonFullName right)
            return EqualityComparer<PersonFullName>.Default.Equals(left, right);

        public static bool operator !=(PersonFullName left, PersonFullName right)
            return !(left == right);

It is so nice to have all of this boiler plate code implemented for us. Without this, two objects with “Julie” and “Lerman” will not be seen as equal with either the Equals or the == operator. But with this code in place, the equality is only based on the two property values and is recognized. JetBrains’ Rider has a similar feature. I’ve not seen anything for VS Code extensions.

Using C#9 Records

Another option is the new C#9 record feature. Learn more about records from author Thomas Claudius Huber at

Rather than implementing IEquatable and adding all of that code to your value object, you can change the type from class to record. I have a duplicate of the original class (before using the Quick Action, which I named PersonFullNameRecord and the only difference is that it’s declared as a record, not a class.

public record PersonFullNameRecord

At compile time, C# will add in all of the equatable and hashcode boilerplate along with some other goodies.  In fact, using the ILSpy extension for Visual Studio, I could see (when debugging) that the compiled class does indeed have that boilerplate built in. The compiled version of the class is a bit long but I’ve included it as a text file in the solution. But it has everything I added into the previous version of the class and some additional methods such as PrintMembers and ToString override that record brings to the party.

C#9 records may not solve every problem in your particular value object, so be sure to test your use cases!

Another path is to inherit from a clever base class that implements all of the needed logic such as Jimmy Bogard’s ValueObject (from 2007) or Pluralsight author, Vladimir Khorikov’s ValueObject or other implementations that expand on this concept.

What About Data Persistence? 

Typically, the values of the value object property are stored in the database table to which the entity is mapped. In my case, an Authors table would include columns for the given and surname values. My data persistence specialty is .NET’s  Entity Framework Core which gives us two ways to persist value objects. The first way is with EF Core’s owned entity (or owned collection) mapping ( This is designed for complex types that are used as properties. The immutability and equality attributes that help us define the value object just come along for the ride. 

Another EF Core feature that works very well for persisting value objects that have a single value is Value Conversion. Imagine you have a value object named OneWordName for people with single names, like Prince. This type has one string property called TheName and some additional helper logic relevant to your domain. With EF Core’s HasConversion mapping method, you could convert the OneWordName object to a string (EF Core will find that single property) and then EF Core can easily map the string to a database schema. HasConversion also has logic to rehydrate data into your objects.

As Sparkjoy said back in 2012, “the beauty of all of this isolation is that we are  establishing a pristine domain layer devoid of tech, leaving only concise business logic,” which is one of the key goals of Domain-Driven Design.

About the author

Julie Lerman is a software coach with 30 years of experience as a developer. She is a long-time Pluralsight author, Microsoft MVP, Microsoft Regional Director and Docker Captain who lives in the hills of Vermont and continues to be known as the world’s top expert in Entity Framework. You can find Julie giving keynotes and hard-core coding sessions on Domain-Driven Design, Azure, Entity Framework and other topics at software conferences around the world. Julie has authored the highly acclaimed “Programming Entity Framework” books from O'Reilly, the MSDN Magazine Data Points column and numerous articles in your favorite programming resources.