Learn what value objects are, their main characteristics and how to use them to write code that is clearly expressed and easy to maintain in your ASP.NET Core app.
When we write code, we are doing more than creating instructions for computers. We are making a business work in the way it was carefully imagined. In this sense, it is fair to consider that we have a great responsibility in our hands.
So, something we should frequently pursue is the quality of the code we write when dealing with complexities. Domain-driven design (DDD) fits perfectly into this task, as it provides several ways to deal with software complexities. One of the main elements of DDD, value objects represent objects that lack their own identity and are recognized by their values.
In this post, we will understand what value objects are and see how to create good value objects in ASP.NET Core, which will be useful for creating more reliable software.
Domain-driven design is an approach to software creation that focuses on providing ways to deal with the complexities encountered during this process, with a focus on the business domain.
DDD helps developers create more modular and less error-prone software by guiding the resolution of complex problems through principles. These principles include bounded contexts, entities, aggregates, value objects, domain services and many others.
If you are unfamiliar with DDD, you can read this post: Getting Started with Domain-Driven Design in ASP.NET Core for an introduction to the main concepts of DDD through a practical approach to creating a DDD-oriented microservice.
The term “value objects” became popular during the 2000s with Martin Fowler’s book Refactoring Improving the Design of Existing Code and became even better known after 2003 with the release of Eric Evans’ book, Domain-Driven Design: Tackling Complexity in the Heart of Software.
In both works, value objects have the same meaning: Immutable entities that represent a concept or description in a domain and that have equality based on the values of their attributes and not on their identity.
In simple terms, value objects are objects that are identified by what they represent and not by an identifying property such as an ID (acronym for Identity).
We can identify value objects through two main characteristics:
1. They have no identity.
This means that they cannot be distinguished from each other based on a unique identifier (such as an ID). Instead, the equality between two value objects is determined by the content present in their attributes.
Imagine the following: two people who live in the same house may have the same address. In this way, even if there are two records in the system representing the same address, they are considered equivalent, because their values (such as street, number, city, state and ZIP code) are the same. In this case, the address is a value object, since its identity does not matter, only the content it represents.
On the other hand, two people who live in the same house are entities, because each person has a unique identity such as an ID and/or a Social Security Number for example, even if they share the same address. This means that value objects can refer to entities, but never have an identity of their own.
2. They are immutable.
It means that once a value object is created, the values of its attributes remain the same throughout its existence.
Immutability is a beneficial effect because, in general, it makes value objects reliable. In this way, the immutability of value objects prevents unexpected changes from affecting other parts of the system that share the same object.
For example, if two customers share the same address and it changes for one customer, this could impact the other customer in an undesirable way. With immutability, instead of changing the original object, you create a new one.
Another positive point is that immutable value objects are inherently thread-safe because their state cannot change after creation. This makes them an excellent choice in systems that run multiple threads simultaneously.
The image below shows the Address value object within the Order aggregation.
Note that Address, unlike Order and Customer, is not treated as an entity, but rather as a value object, without an ID property.
Value objects bring several benefits to application design. Among these benefits, we can highlight:
Domain expressiveness: Value objects make the domain model more expressive. For example, a physical address or an email can be represented as a value object, encapsulating validation logic and specific formatting, such as requiring @
for email addresses. Keeping these rules within the value object class makes the model more expressive and organized.
Immutability: Value objects are designed to be immutable, so that, once created, their values cannot be changed. This behavior avoids unexpected side effects and makes the code more predictable.
Separation of concerns: Using value objects helps separate concerns from domain logic. This keeps the logic where it belongs, within the model.
More reliable domains: Value objects reduce the likelihood of errors related to inconsistent data. For example, instead of using strings or raw numbers to represent concepts like currency or dates, you use a value object, which has all the validation and formatting rules.
To implement the value object, we will use the address example seen above. So, we will create a simple API then create a generic class with the main elements of a value object, and then create the address class that inherits from this generic class.
To keep the focus on the post’s objective, some details of the project’s implementation will not be covered, but you can access the complete application code in this GitHub repository: Order Service source code.
So, the first step is to run the command to create the application. You can use the command below to do it:
dotnet new web -n OrderService
The NuGet packages used in the application are as follows:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
</ItemGroup>
Open the project and create a new folder called “Models” and, inside it, create the following class:
namespace OrderService.Models;
public abstract class BaseValueObject
{
protected static bool EqualOperator(BaseValueObject left, BaseValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, right) || left.Equals(right);
}
protected static bool NotEqualOperator(BaseValueObject left, BaseValueObject right) =>
!EqualOperator(left, right);
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (BaseValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
}
Here we create the generic class BaseValueObject
, which will serve as a base for other classes that are value objects. It is abstract because as a generic base for creating value objects; it does not need to be instantiated directly. Furthermore, it requires that derived classes define the specific details present in it, such as equality logic.
Now let’s analyze each method present in the class:
1. EqualOperator
protected static bool EqualOperator(BaseValueObject left, BaseValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, right) || left.Equals(right);
}
This method checks the equality between two objects of type BaseValueObject
, through a custom implementation of the equality operator (==
). To do this, it uses ReferenceEquals
to check if one or both references are null. The ^
(XOR) operator returns true if only one of the references is null. If both references are non-null, then the comparison returns true if they are the same instance (same reference). Finally, it uses the Equals method to compare the values of the properties.
2. NotEqualOperator
protected static bool NotEqualOperator(BaseValueObject left, BaseValueObject right) =>
!EqualOperator(left, right);
This method checks the difference between two objects of type BaseValueObject, using a custom approach to the difference operator (!=).
To check the difference between the objects, use the negation of the result of EqualOperator. If EqualOperator returns true, it means that the objects are equal, so the method returns false due to the negation operator (!) and vice versa.
3. GetEqualityComponents
protected abstract IEnumerable<object> GetEqualityComponents();
This method defines the components that determine the equality of a value object, returning an IEnumerable
of objects.
Each derived class must implement this method and provide the values that represent the state of the object. For example, for an Address class, the components could be Street, City and ZipCode.
4. Equals
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (BaseValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
This method overrides the Equals method to compare two objects. It does this by checking whether the given object is null or of a different type; if so, it returns false. Otherwise, it casts the object to a BaseValueObject
and compares the relevant components using SequenceEqual
. SequenceEqual
compares sequences element by element, verifying that the components are equal in both order and value.
5. GetHashCode
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
This method overrides GetHashCode
to provide a hashcode consistent with the definition of equality. To do so, it uses the equality components (defined in GetEqualityComponents
) to generate the hash code. Here the Select
extension method gets the hash code of each component (x.GetHashCode()
), returning 0 for null components. While the second Aggregate
extension method combines the hash codes using the XOR operator (^
), for a unique hash.
In this way, the methods of the BaseValueObject
class keep the value objects that implement it working in a consistent and reliable manner, respecting the essential characteristics of value objects in a domain model that follows the DDD premises.
Now let’s create the Address
class that represents the value object, and which will inherit from the base class defined previously. So, in the “Models” folder, create the following class:
namespace OrderService.Models;
public class Address : BaseValueObject
{
public string Street { get; private set; }
public long Number { get; private set; }
public string City { get; private set; }
public string State { get; private set; }
public string Country { get; private set; }
public string ZipCode { get; private set; }
public string Complement { get; private set; }
public Address() { }
public Address(string street, long number, string city, string state, string country, string zipCode, string complement)
{
Street = street;
Number = number;
City = city;
State = state;
Country = country;
ZipCode = zipCode;
Complement = complement;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return Number;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
yield return Complement;
}
}
Let’s analyze the class Address
.
First, the class inherits from BaseValueObject
, which implements the common functionalities for value objects. Note that there is no ID here—after all, value objects do not have their own identity. Another detail is that all properties have only one public getter and one private setter, which means that their value can only be set within the class, but they can be accessed externally.
Additionally, the GetEqualityComponents()
method overrides the BaseValueObject
base class method. This method is used to base the equality comparison between two Address objects on their property values rather than their object references. In this case, the method returns all the components (street, number, city, state, country, postal code and complement) that will be used to compare the equality between two Address objects.
So, we have a good example of a value object for customer addresses. Something you might wonder is, since value objects are immutable, shouldn’t they be read-only, meaning having get-only properties?
Yes, but value objects are usually serialized and deserialized for various purposes, in this case, if they were read-only, they would prevent the deserializer from assigning values. So, leave them as a private set, and they will become useful for most scenarios.
Consider the code below:
var homeAddress = new Address(
"123 Elm Street",
42,
"Springfield",
"Illinois",
"USA",
"62704",
"Apartment 4B"
);
var businessAddress = new Address(
"123 Elm Street",
42,
"Springfield",
"Illinois",
"USA",
"62704",
"Apartment 4B"
);
// 1 Using EqualityComparer
Console.WriteLine(EqualityComparer<Address>.Default.Equals(homeAddress, businessAddress)); // True
// 2 Using object.Equals
Console.WriteLine(object.Equals(homeAddress, businessAddress)); // True
// 3 Using Equals
Console.WriteLine(businessAddress.Equals(businessAddress)); // True
// 4. Using == operator
Console.WriteLine(homeAddress == businessAddress); // False
Note that in the first three operations, the homeAddress
and businessAddress
value objects are equal, but in the fourth using the ==
equality operator they are different.
This happens because by default, the ==
operator in classes checks object references, not logical equality. Since homeAddress
and businessAddress
are different instances, even if their values are equal, the operator evaluates to false. The ValueObject
type is an abstract class. But, in this example, it does not overload the ==
and !=
operators. You can work around this by overloading the operators.
In the BaseValueObject
class, add the following methods:
public static bool operator ==(BaseValueObject objectOne, BaseValueObject objectTwo)
{
return EqualOperator(objectOne, objectTwo);
}
public static bool operator !=(BaseValueObject objectOne, BaseValueObject objectTwo)
{
return NotEqualOperator(objectOne, objectTwo);
}
Now, the objects are compared logically (based on their properties). So if you run the method again, the output will be true for all cases:
// 1. Using EqualityComparer
Console.WriteLine(EqualityComparer<Address>.Default.Equals(homeAddress, businessAddress)); // True
// 2. Using object.Equals
Console.WriteLine(object.Equals(homeAddress, businessAddress)); // True
// 3. Using Equals
Console.WriteLine(businessAddress.Equals(businessAddress)); // True
// 4. Using the == operator overload
Console.WriteLine(homeAddress == businessAddress); // True
Entity Framework Core is focused on entities with identity, so to maintain value objects that are not entities in the database using EF Core, some configuration is necessary.
To see how to add the Address value object to the database using EF Core, let’s create an Order class that will contain two types of Address: ShippingAddress
and BillingAddress
.
Inside the “Models” folder, create the class below:
namespace OrderService.Models;
public class Order
{
public int Id { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
}
Then, create a new folder called “Data” and inside it, create the class below:
using Microsoft.EntityFrameworkCore;
using OrderService.Models;
namespace OrderService.Data;
public class OrderDb: DbContext
{
public OrderDb(DbContextOptions<OrderDb> options): base(options) { }
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
entity.OwnsOne(o => o.ShippingAddress, sa =>
{
sa.Property(a => a.Street).HasColumnName("ShippingStreet");
sa.Property(a => a.Number).HasColumnName("ShippingNumber");
sa.Property(a => a.City).HasColumnName("ShippingCity");
sa.Property(a => a.State).HasColumnName("ShippingState");
sa.Property(a => a.Country).HasColumnName("ShippingCountry");
sa.Property(a => a.ZipCode).HasColumnName("ShippingZipCode");
sa.Property(a => a.Complement).HasColumnName("ShippingComplement");
});
entity.OwnsOne(o => o.BillingAddress, ba =>
{
ba.Property(a => a.Street).HasColumnName("BillingStreet");
ba.Property(a => a.Number).HasColumnName("BillingNumber");
ba.Property(a => a.City).HasColumnName("BillingCity");
ba.Property(a => a.State).HasColumnName("BillingState");
ba.Property(a => a.Country).HasColumnName("BillingCountry");
ba.Property(a => a.ZipCode).HasColumnName("BillingZipCode");
ba.Property(a => a.Complement).HasColumnName("BillingComplement");
});
});
}
}
Note that here, we only have one DbSet
for the Order entity. If we did the same for the Address class, it would not work because Address is not an entity, it is a component of Order, so it is not accessible in EF Core.
So, to get around the fact that Address will not be an entity in the database, we make it an order component through the OnModelCreating
method that declares that the Order entity will have all the Address properties, but with different names, one property for each ShippingAddress
and BillingAddress
context. This is done through the following configuration:
entity.OwnsOne(o => o.ShippingAddress, sa =>
{
sa.Property(a => a.Street).HasColumnName("ShippingStreet");
//...other properties
});
entity.OwnsOne(o => o.BillingAddress, ba =>
{
ba.Property(a => a.Street).HasColumnName("BillingStreet");
//...other properties
});
Now, if you run the EF Core commands for data generation you will get the following result:
To insert a new record, including the value object, no additional configuration is required. Simply create a POST endpoint and pass the new request object to the Orders object:
app.MapPost("/order/create", async (Order order, OrderDb db) =>
{
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Ok(order);
});
{
"shippingAddress": {
"street": "123 Elm Street",
"number": 42,
"city": "Springfield",
"state": "Illinois",
"country": "USA",
"zipCode": "62704",
"complement": "Apartment 4B"
},
"billingAddress": {
"street": "456 Oak Avenue",
"number": 100,
"city": "Seattle",
"state": "Washington",
"country": "USA",
"zipCode": "98101",
"complement": "Suite 500"
}
}
And to retrieve only the value object you can use a LINQ query. In the example below, only the properties related to ShippingAddress are returned.
app.MapGet("order/shippingAddresses", (OrderDb db) =>
{
var shippingAddresses = db.Orders
.Select(o => new
{
o.ShippingAddress.Street,
o.ShippingAddress.Number,
o.ShippingAddress.City,
o.ShippingAddress.State,
o.ShippingAddress.Country,
o.ShippingAddress.ZipCode,
o.ShippingAddress.Complement
})
.ToList();
return shippingAddresses != null ? Results.Ok(shippingAddresses) : Results.NotFound();
});
The image below shows the records inserted into the Orders table:
Value objects are a fundamental component of domain-driven design, as they help developers convey the real meaning of objects that do not have their own identity, such as addresses, currencies, date ranges, etc.
In this post, we looked at value objects, their main characteristics and how to implement them in an ASP.NET Core application. We also saw details of this implementation, such as how to deal with value objects in EF Core, which focuses on entity manipulation.
When dealing with scenarios where you need to implement an object that does not have an identity and must be immutable, consider using a value object for this.
I hope this post helps you start implementing value objects and creating more reliable software that follows DDD principles.