DEV Community

mohamed Tayel
mohamed Tayel

Posted on

EFCore Tutorial P9:Understanding and Implementing Value Objects

Meta Description: Discover how to implement Value Objects in EF Core, including how to use owned types, configure value objects, and update your database with migrations for cleaner and more maintainable domain models.

In Domain-Driven Design (DDD), Value Objects are essential for modeling concepts in a domain that do not have a distinct identity. Unlike entities, value objects are immutable and are defined by their attributes rather than a unique ID. This article explores how to implement value objects in Entity Framework Core (EF Core) and includes practical steps for managing them in your database using migrations.


What is a Value Object?

A Value Object is a model component that is defined by its properties rather than by an identity. It doesn't have an ID like entities, and two value objects are considered equal if their attributes are the same.

Characteristics of Value Objects:

  1. Immutability: Once created, the data in a value object should not change.
  2. Equality by Properties: Two value objects are equal if all their properties match.
  3. No Identity: Value objects don’t have an ID, they are identified by the data they contain.

For example, an Address value object can encapsulate related properties like Street, City, and ZipCode instead of scattering them across an entity.


Why Use Value Objects?

Value objects help encapsulate related properties, which improves domain modeling by keeping data grouped logically. They also ensure that related properties remain consistent and make the model cleaner and more maintainable.

Example scenarios for value objects:

  • Address: Contains Street, City, and ZipCode.
  • Money: Combines Amount and Currency.

Implementing Value Objects in EF Core

In EF Core, value objects are implemented as owned types. This means that their lifecycle is tied to the entity that owns them. Let’s explore the steps to implement a value object in EF Core.


Step 1: Define the Value Object

Define a value object called Address to represent an address with Street, City, and ZipCode.

public class Address
{
    public string Street { get; private set; }
    public string City { get; private set; }
    public string ZipCode { get; private set; }

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

    // Override equality and GetHashCode for value equality
    protected bool Equals(Address other)
    {
        return Street == other.Street && City == other.City && ZipCode == other.ZipCode;
    }

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

    public override int GetHashCode()
    {
        return HashCode.Combine(Street, City, ZipCode);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, the Address value object encapsulates the address-related properties and overrides Equals and GetHashCode to enable equality by value.


Step 2: Use the Value Object in an Entity

Next, define a Customer entity that owns the Address value object.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; private set; }

    public Customer(string name, Address address)
    {
        Name = name;
        Address = address;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Customer entity now has an Address value object as one of its properties.


Step 3: Configure the Value Object in EF Core

To let EF Core know that Address is an owned type, configure it using the OwnsOne method in the CustomerConfiguration.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Name).IsRequired();

        // Configure Address as a value object (owned type)
        builder.OwnsOne(c => c.Address, a =>
        {
            a.Property(p => p.Street).HasColumnName("Street").IsRequired();
            a.Property(p => p.City).HasColumnName("City").IsRequired();
            a.Property(p => p.ZipCode).HasColumnName("ZipCode").IsRequired();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

In this configuration, OwnsOne tells EF Core to treat Address as part of the Customer entity, with its properties (Street, City, and ZipCode) being stored in the Customers table.


Step 4: Apply Migration to Update the Database

After adding the value object, you need to update your database schema using migrations.

  1. Add a Migration: Open your terminal or Package Manager Console and run the following command:
   dotnet ef migrations add AddAddressToCustomer
Enter fullscreen mode Exit fullscreen mode

This generates a migration file that updates the database schema to include Address in the Customer entity.

  1. Update the Database: Apply the migration to your database:
   dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

This command runs the migration, updating the database with the new Address columns (Street, City, ZipCode) in the Customers table.


Step 5: Creating a Customer Service

To manage customers and their associated Address value objects, we can introduce a CustomerService that handles typical CRUD operations.

public class CustomerService
{
    private readonly AppDbContext _context;

    public CustomerService(AppDbContext context)
    {
        _context = context;
    }

    // Create a new customer
    public void CreateCustomer(string name, string street, string city, string zipCode)
    {
        var address = new Address(street, city, zipCode);
        var customer = new Customer(name, address);

        _context.Customers.Add(customer);
        _context.SaveChanges();
    }

    // Get a customer by ID
    public Customer GetCustomerById(int id)
    {
        return _context.Customers
                       .Include(c => c.Address) // Include the Address value object
                       .FirstOrDefault(c => c.Id == id);
    }

    // Update customer's address
    public void UpdateCustomerAddress(int customerId, string newStreet, string newCity, string newZipCode)
    {
        var customer = _context.Customers.Find(customerId);
        if (customer != null)
        {
            var newAddress = new Address(newStreet, newCity, newZipCode);
            customer.Address = newAddress;  // Replace the old address with the new one
            _context.SaveChanges();
        }
    }

    // Delete a customer
    public void DeleteCustomer(int customerId)
    {
        var customer = _context.Customers.Find(customerId);
        if (customer != null)
        {
            _context.Customers.Remove(customer);
            _context.SaveChanges();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This service encapsulates the logic for managing Customer and Address data, keeping the code clean and maintainable.


Step 6: Usage in Code

Here’s how you can use the CustomerService to manage customers and their addresses:

public static void Main(string[] args)
{
    using (var context = new AppDbContext())
    {
        var customerService = new CustomerService(context);

        // Create a new customer
        customerService.CreateCustomer("Alice Johnson", "456 Maple St", "Los Angeles", "90001");

        // Get a customer
        var customer = customerService.GetCustomerById(1);
        if (customer != null)
        {
            Console.WriteLine($"Customer: {customer.Name}, Address: {customer.Address.Street}, {customer.Address.City}, {customer.Address.ZipCode}");
        }

        // Update the customer's address
        customerService.UpdateCustomerAddress(1, "789 Oak St", "Los Angeles", "90002");

        // Delete the customer
        customerService.DeleteCustomer(1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Advantages of Value Objects

  • Encapsulation: By grouping related properties into a value object, the domain model remains clean and expressive.
  • Immutability: Value objects are immutable, ensuring consistency across the application.
  • Equality by Value: Two value objects are considered equal if their properties are the same, which makes comparison easier.

Conclusion

Value Objects are a powerful way to model parts of your domain that are defined by their properties rather than identity. In EF Core, they are implemented as owned types, which simplifies their management. With a CustomerService in place, you can handle Customer entities and their value objects in a clean, maintainable way. Don’t forget to apply migrations after adding value objects to ensure your database schema reflects the changes.

Top comments (0)