How to use value objects in C#

By FoxLearn 1/6/2025 8:02:08 AM   29
In C#, objects are classified as either value types or reference types.

A value type holds its actual data, while a reference type holds a reference to an object. The main difference lies in assignment behavior: when assigning a value type, the data is copied; when assigning a reference type, only the reference (address) is copied, so both references point to the same object.

What are Value Objects in C#?

In C#, a value object is an object identified solely by the state of its properties, without any inherent identity. In domain-driven design (DDD), value objects are immutable, meaning their state cannot be changed after creation. Any modification results in a new instance. This immutability allows value objects to be interchangeable if their values are the same, offering performance benefits through object reuse. Unlike value objects, entity objects have distinct identities and mutable states.

Example of Value Objects in C#

For example, Consider the following Product class.

public class Product
{
    public string ProductName { get; set; }
    public string ProductCode { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    
    public Product(string productName, string productCode, decimal price, int stockQuantity)
    {
        ProductName = productName;
        ProductCode = productCode;
        Price = price;
        StockQuantity = stockQuantity;
    }
}

You can create an instance of this class and assign values to its properties using the following code. However, because the Product class here exposes public setters, you could easily change the values of its properties from outside the class. This violates one of the basic features of value objects immutability.

To fix this, we can redesign the Product class as follows:

public class Product
{
    public string ProductName { get; private set; }
    public string ProductCode { get; private set; }
    public decimal Price { get; private set; }
    public int StockQuantity { get; private set; }

    public Product(string productName, string productCode, decimal price, int stockQuantity)
    {
        ProductName = productName;
        ProductCode = productCode;
        Price = price;
        StockQuantity = stockQuantity;
    }
}

In this example, the properties of the Product class now contain private setters, preventing external modifications to the property values. If you attempt to modify the properties from outside the class, it will result in a compile-time error.

Alternatively, in C# you can implement value objects using records, which inherently support immutability.

Here’s how you can define a Product value object using a record:

public record Product(string ProductName, string ProductCode, decimal Price, int StockQuantity);

This syntax is more concise and automatically generates the properties with getters only, ensuring the object is immutable by default.

Best practices for using value objects in C#

When working with value objects in C#, follow these best practices:

  1. Implement equality checks based on value to avoid inconsistencies and errors.
  2. Use value objects when appropriate for representing domain concepts.
  3. Use value objects carefully, as they can introduce performance overhead.
  4. Implement validation logic in the constructor to enforce business rules when creating a value object.
  5. Follow established patterns and naming conventions to maintain clarity, consistency, and readability in your value object implementations.

Value objects encapsulate primitive types and have two key characteristics: they lack identity and are immutable. This approach ensures data integrity, enforces strong typing, reduces errors, and enhances code readability. Leveraging value objects can improve the clarity and maintainability of your C# code by offering a more expressive representation of domain concepts.

To implement value objects in C#, typically override equality methods (Equals() and GetHashCode()), and define appropriate comparison operators. A base class with these methods is often used as a template for all value object implementations.