3 Different Ways to Implement Value Object in C# 10

Let’s find out how to implement value objects and map them to a database table with EF Core 6.0.

3 Different Ways to Implement Value Object in C# 10

What is Value Object

A value object is an object without an explicit identifier. Its first main characteristic is that it does not require an identity.

The second principal characteristic is that value objects must be immutable. It means that once value object properties are initialized, we can’t change their values. You can change them only by introducing a new value object with different values in its properties.

A good example is an address. It consists of city, street, and zip code. Once you change one of them, you are changing the whole address. It is whole new Address().

The third main characteristic of value objects is also value equality. Value objects are equal once their type and the values of their properties are equal. Every implementation of a value object has to support such behavior.

Value Object is one of Domain-Driven Design patterns as Entity and Aggregate for domain modeling. I already wrote a few words about DDD in my other article below.

If you are new to DDD, you can still take exciting knowledge from this article. But I would also recommend looking at the Pluralsight course Domain-Driven Design Fundamentals by Julie Lerman and Steve Smith.

Different Ways of Implementation

Value Object Parent

One of the first typical implementations of value objects in C# is the ValueObject abstract class. Every class which inherits from it was considered a value object.

public abstract class ValueObject
{
    protected static bool EqualOperator(ValueObject left, ValueObject right)
    {
        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
        {
            return false;
        }
        return ReferenceEquals(left, null) || left.Equals(right);
    }

    protected static bool NotEqualOperator(ValueObject left, ValueObject right)
    {
        return !(EqualOperator(left, right));
    }

    protected abstract IEnumerable<object> GetEqualityComponents();

    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var other = (ValueObject)obj;

        return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Select(x => x != null ? x.GetHashCode() : 0)
            .Aggregate((x, y) => x ^ y);
    }
    // Other utility methods
}

You can see an overridden GetHashCode method. This method is used to return the hash code for the object instance. Hash codes are used to identify an object in a hash-based collection. The method provides hash code for algorithms that need quick checks of object equality.

If we want to compare objects as value objects, we have to compute the hash code based on hash codes of its properties. Values of properties are provided by the GetEqualityComponents abstract method.

Every child of the ValueObject parent has to implement the GetEqualityComponents method.

public class Address : ValueObject
{
  public string Street { get; init; }
  public string ZipCode { get; init; }
  public string City { get; init; }

  public Address(string street, strin zipCode, string city){
    Street = street;
    ZipCode = zipCode;
    City = city;
  }

  protected override IEnumerable<object> GetEqualityComponents(){
    yield return Street;
    yield return ZipCode;
    yield return City;
  }
}

Note that I am using the init keyword instead of the specific set keyword at each property. By this, you define the property as Init Only, and you can be sure about its immutability.

Records

Records have been with us since C# 9. You can use the record keyword to define a reference type that provides built-in functionality for encapsulating data. Built-in functionality is added at compilation time by the compiler itself.

With the record keyword, we can use positional syntax. It is a shorter way to define an automatically immutable class:

public record class Address(string Street, string ZipCode, string City);

/*
EQUALS:
public class Adress{
  public string Street { get; init; }
  public string ZipCode { get; init; }
  public string City { get; init; }

  public Address(string street, string zipcode, string city){
    Street = street;
    ZipCode = zipcode;
    City = city;
  }
}
*/

So by using positional syntax, we meet one of the main characteristics — immutability.

Another characteristic is value equality. Microsoft docs say about record classes this:

For record types, including record struct and readonly record struct, two objects are equal if they are of the same type and store the same values.

Value equality behavior is secured thanks to the built-in overriding of the Equals method at compile time.

The last characteristic is The potency of identifier. Since we will not introduce any unique property, we should be good.

We can say that the record class is a quick and straightforward way to define value objects.

Structs

Struct is a value type. It means that the struct type variable contains an instance directly, not a reference. The variable values are copied on assignment, passing an argument to a method, and returning a method result.

On the other hand, the reference type variable contains a reference to an instance. Assign statement does not copy the instance but only passes its reference. Also, passing a reference type variable to a method means that modifications in a method are made on the same instance, not on its copy.

The struct can also be defined with the keyword record and positional syntax.

public record struct Address(string Street, string ZipCode, string City);

So again, we meet all the three main characteristics — immutability, the potency of identifier, and value equality. So struct is another reliable way to implement value objects.

Summary

Value object parent is the most complex implementation and I would recommend it for more advanced DDD developers. Vladimir Khorikov did a great article about Value object parent advantages over records here.

I am using value objects on a lower level, and I am more “pro- records,” so I will focus on them in my last words.

When we should use record class instead of record struct and vice versa? Both meet the main requirements for value object implementation. Struct is allocated on the stack. Structs should be small. Passing the data around across methods can be costly if it is too large because we are creating a new instance of the value type with every passing. Use them when there is a limited amount of data being held.

A class is allocated on the heap. It is a reference type, so passing the object through methods is not that costly. If you want to define a more significant value object, you should use a class.

It is also more about when to use reference type and value type. Microsoft documentation contains an article dedicated to right this question here.

Value objects are immutable variables predefined to be held in an entity and persist in a database. The number of passing it across methods will always be higher, meaning that record classes (reference types) are more suitable for value objects implementations than record structs (value types).

Value Objects and EF Core

Owned Types

The convenient feature of Entity Framework Core for mapping value objects is Owned Types. The definition from Microsoft documentation says:

EF Core allows you to model entity types that can only ever appear on navigation properties of other entity types. These are called owned entity types. The entity containing an owned entity type is its owner.

So, from the EF Core point of view, the value object can’t exist without its owner. In most cases, the owner is some entity. Imagine a class Person as an entity (owner) with the property of type Address as its value type (owned type).

public class Person
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}

public record class Address(string City, string Street, string ZipCode);

For mapping the entity Person to the database table, I will use fluent model builder and directly in overridden method OnModelCreating in DbContext class.

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>(b =>
        {
            b.ToTable("Persons").HasKey(x => x.Id);
            b.Property(x => x.Name).HasMaxLength(255).HasDefaultValue(string.Empty);
            b.Property(x => x.LastName).HasMaxLength(255).HasDefaultValue(string.Empty);
            b.OwnsOne(x => x.Address, sb =>
            {
                sb.Property(x => x.City).HasMaxLength(255).HasDefaultValue(string.Empty);
                sb.Property(x => x.Street).HasMaxLength(255).HasDefaultValue(string.Empty);
                sb.Property(x => x.ZipCode).HasMaxLength(255).HasDefaultValue(string.Empty);
            });
        });
    }
}

As you can see, I am saying to EF Core that Person owns one address and then defines address properties in its sub builder. The resulting database table looks like this.

image.png

The values of address properties are stored in columns of table Persons with the particular directive for its names — “ValueObjectName_PropertyName.”

But what if I have a collection of addresses? If you have a collection of value objects in an entity, you should use the method OwnsMany and EF Core will map them in its table with foreign key PersonId.

b.OwnsMany(x => x.Addresses, sb =>
{
    sb.Property(x => x.City)
        .HasMaxLength(255)
        .HasDefaultValue(string.Empty);

    sb.Property(x => x.Street)
        .HasMaxLength(255)
        .HasDefaultValue(string.Empty);

    sb.Property(x => x.ZipCode)
        .HasMaxLength(255)
        .HasDefaultValue(string.Empty);
});

image.png

But if you choose struct for your value object, you can’t yet use owned types for it. You must serialize the value object to JSON and store it in a varchar column. But it will maybe change with EF Core 7.0.

It is pretty simple to introduce conversion from address to JSON, but you will not read the database more conveniently, like when the values of address properties are each stored in its column.

modelBuilder.Entity<Person>()
.Property(e => e.Address)
.HasConversion(
    v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
    v => JsonSerializer.Deserialize<Address>(v, (JsonSerializerOptions)null));

image.png

Since EF Core 6.0, we can use the feature Pre-convention model configuration and define a conversion from type Address to JSON globally by introducing ValueConverter for value object type and configuring it in ConfigureConventions method in DbContext class.

public class AddressConverter : ValueConverter<Address, string>
{
    public AddressConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
            v => JsonSerializer.Deserialize<Address>(v, (JsonSerializerOptions)null))
    {
    }
}
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        configurationBuilder.Properties<Address>().HaveConversion<AddressConverter>();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>(b =>
        {
            b.ToTable("Persons").HasKey(x => x.Id);
            b.Property(x => x.Name)
                .HasMaxLength(255)
                .HasDefaultValue(string.Empty);

            b.Property(x => x.LastName)
                .HasMaxLength(255)
                .HasDefaultValue(string.Empty);

            b.Property(e => e.Address);
        });
    }
}

Sources

itixo-logo-blue.png