Implementing Domain-Driven Design with C# and Entity Framework

Implementing Domain-Driven Design with C# and Entity Framework When building complex software systems, it can be tempting to jump straight into coding without a clear understanding of the problem domain. This often leads to brittle, hard-to-maintain codebases that fail to meet the needs of the business. Enter Domain-Driven Design (DDD)—a methodology that emphasizes collaboration between technical and business stakeholders to design software that truly reflects the core business domain. In this blog post, we’ll explore how to implement Domain-Driven Design principles in C# using Entity Framework (EF). We’ll cover key concepts like aggregates, repositories, value objects, and domain services, complete with practical code examples. Whether you’re a seasoned developer or new to DDD, this guide will help you write cleaner, more maintainable code. Why Domain-Driven Design? Imagine building software for an online bookstore. Without DDD, you might start by creating tables for Books, Customers, and Orders. While this works for CRUD operations, it often ignores the complexities of the business domain—like ensuring a book can’t be ordered if it’s out of stock, or calculating discounts based on customer loyalty. These rules end up scattered across the application, leading to duplication and bugs. DDD addresses this by focusing on the domain—the core business logic—and organizing code around it. It introduces concepts like ubiquitous language, where developers and business experts use the same terms to describe the system, and bounded contexts, which ensure each part of the system is modeled independently. Core Concepts of Domain-Driven Design Before diving into code, let’s briefly define some key DDD concepts: Aggregate: A cluster of domain objects that are treated as a single unit. Aggregates enforce business rules and guarantee consistency. Value Object: An immutable object that represents a value, like a Money or Address, without a unique identity. Repository: A pattern for accessing aggregates from a data store, abstracting away persistence concerns. Domain Service: A service that encapsulates domain logic not naturally belonging to an aggregate or value object. We’ll explore these concepts in action using C# and Entity Framework. Implementing DDD Concepts in C# and Entity Framework 1. Defining the Aggregate Root An aggregate root is the entry point to a group of related objects in the domain. For our bookstore example, an Order can serve as an aggregate root, ensuring that all changes to OrderLine entities go through the Order aggregate. Here’s how we can implement it: public class Order { private readonly List _orderLines = new(); public Guid Id { get; private set; } public DateTime OrderDate { get; private set; } public IReadOnlyCollection OrderLines => _orderLines.AsReadOnly(); protected Order() { } // For EF Core public Order(DateTime orderDate) { Id = Guid.NewGuid(); OrderDate = orderDate; } public void AddOrderLine(Guid productId, int quantity, decimal unitPrice) { if (quantity line.GetLineTotal()); } } public class OrderLine { public Guid ProductId { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; } protected OrderLine() { } // For EF Core public OrderLine(Guid productId, int quantity, decimal unitPrice) { ProductId = productId; Quantity = quantity; UnitPrice = unitPrice; } public decimal GetLineTotal() { return Quantity * UnitPrice; } } Here, Order is the aggregate root, and OrderLine is part of the aggregate. Any changes to OrderLine must go through the Order class, ensuring consistency. 2. Using Value Objects A value object represents a concept with no unique identity. For example, an Address for a customer: public class Address { public string Street { get; } public string City { get; } public string Country { get; } public Address(string street, string city, string country) { if (string.IsNullOrWhiteSpace(street) || string.IsNullOrWhiteSpace(city) || string.IsNullOrWhiteSpace(country)) throw new ArgumentException("Address fields cannot be empty."); Street = street; City = city; Country = country; } public override bool Equals(object? obj) { if (obj is not Address other) return false; return Street == other.Street && City == other.City && Country == other.Country; } public override int GetHashCode() { return HashCode.Combine(Street, City, Country); } } Value objects are immutable, meaning their state cannot change after creation. Use them to encapsulate concepts like addresses, money, or measurements. 3. Implementing Repositories A repository mediates betwee

Jun 7, 2025 - 14:20
 0
Implementing Domain-Driven Design with C# and Entity Framework

Implementing Domain-Driven Design with C# and Entity Framework

When building complex software systems, it can be tempting to jump straight into coding without a clear understanding of the problem domain. This often leads to brittle, hard-to-maintain codebases that fail to meet the needs of the business. Enter Domain-Driven Design (DDD)—a methodology that emphasizes collaboration between technical and business stakeholders to design software that truly reflects the core business domain.

In this blog post, we’ll explore how to implement Domain-Driven Design principles in C# using Entity Framework (EF). We’ll cover key concepts like aggregates, repositories, value objects, and domain services, complete with practical code examples. Whether you’re a seasoned developer or new to DDD, this guide will help you write cleaner, more maintainable code.

Why Domain-Driven Design?

Imagine building software for an online bookstore. Without DDD, you might start by creating tables for Books, Customers, and Orders. While this works for CRUD operations, it often ignores the complexities of the business domain—like ensuring a book can’t be ordered if it’s out of stock, or calculating discounts based on customer loyalty. These rules end up scattered across the application, leading to duplication and bugs.

DDD addresses this by focusing on the domain—the core business logic—and organizing code around it. It introduces concepts like ubiquitous language, where developers and business experts use the same terms to describe the system, and bounded contexts, which ensure each part of the system is modeled independently.

Core Concepts of Domain-Driven Design

Before diving into code, let’s briefly define some key DDD concepts:

  1. Aggregate: A cluster of domain objects that are treated as a single unit. Aggregates enforce business rules and guarantee consistency.
  2. Value Object: An immutable object that represents a value, like a Money or Address, without a unique identity.
  3. Repository: A pattern for accessing aggregates from a data store, abstracting away persistence concerns.
  4. Domain Service: A service that encapsulates domain logic not naturally belonging to an aggregate or value object.

We’ll explore these concepts in action using C# and Entity Framework.

Implementing DDD Concepts in C# and Entity Framework

1. Defining the Aggregate Root

An aggregate root is the entry point to a group of related objects in the domain. For our bookstore example, an Order can serve as an aggregate root, ensuring that all changes to OrderLine entities go through the Order aggregate.

Here’s how we can implement it:

public class Order
{
    private readonly List<OrderLine> _orderLines = new();

    public Guid Id { get; private set; }
    public DateTime OrderDate { get; private set; }
    public IReadOnlyCollection<OrderLine> OrderLines => _orderLines.AsReadOnly();

    protected Order() { } // For EF Core

    public Order(DateTime orderDate)
    {
        Id = Guid.NewGuid();
        OrderDate = orderDate;
    }

    public void AddOrderLine(Guid productId, int quantity, decimal unitPrice)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be greater than zero.");

        _orderLines.Add(new OrderLine(productId, quantity, unitPrice));
    }

    public decimal GetTotalAmount()
    {
        return _orderLines.Sum(line => line.GetLineTotal());
    }
}

public class OrderLine
{
    public Guid ProductId { get; private set; }
    public int Quantity { get; private set; }
    public decimal UnitPrice { get; private set; }

    protected OrderLine() { } // For EF Core

    public OrderLine(Guid productId, int quantity, decimal unitPrice)
    {
        ProductId = productId;
        Quantity = quantity;
        UnitPrice = unitPrice;
    }

    public decimal GetLineTotal()
    {
        return Quantity * UnitPrice;
    }
}

Here, Order is the aggregate root, and OrderLine is part of the aggregate. Any changes to OrderLine must go through the Order class, ensuring consistency.

2. Using Value Objects

A value object represents a concept with no unique identity. For example, an Address for a customer:

public class Address
{
    public string Street { get; }
    public string City { get; }
    public string Country { get; }

    public Address(string street, string city, string country)
    {
        if (string.IsNullOrWhiteSpace(street) || string.IsNullOrWhiteSpace(city) || string.IsNullOrWhiteSpace(country))
            throw new ArgumentException("Address fields cannot be empty.");

        Street = street;
        City = city;
        Country = country;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Address other) return false;
        return Street == other.Street && City == other.City && Country == other.Country;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Street, City, Country);
    }
}

Value objects are immutable, meaning their state cannot change after creation. Use them to encapsulate concepts like addresses, money, or measurements.

3. Implementing Repositories

A repository mediates between the domain and data layer, providing an abstraction over Entity Framework.

public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
    Task AddAsync(Order order);
    Task SaveChangesAsync();
}

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

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

    public async Task<Order?> GetByIdAsync(Guid id)
    {
        return await _context.Orders
            .Include(o => o.OrderLines)
            .FirstOrDefaultAsync(o => o.Id == id);
    }

    public async Task AddAsync(Order order)
    {
        await _context.Orders.AddAsync(order);
    }

    public async Task SaveChangesAsync()
    {
        await _context.SaveChangesAsync();
    }
}

Repositories abstract away database operations, making it easier to test and refactor your code.

4. Adding Domain Services

A domain service contains business logic that doesn’t naturally belong to an aggregate or value object. For example:

public class DiscountService
{
    public decimal CalculateDiscount(Order order)
    {
        if (order.GetTotalAmount() > 100)
        {
            return order.GetTotalAmount() * 0.10m; // 10% discount
        }

        return 0;
    }
}

Domain services help keep your aggregates focused and cohesive.

Common Pitfalls and How to Avoid Them

  1. Ignoring the Ubiquitous Language: Always involve domain experts to ensure your code reflects the business language.
  2. Overengineering: Apply DDD selectively to complex domains. For simple CRUD apps, it may be overkill.
  3. Anemic Domain Models: Avoid models that only hold data without behavior. Encapsulate business logic in your aggregates.
  4. Leaking Persistence Details: Use repositories to hide Entity Framework from the domain layer.

Key Takeaways and Next Steps

  • Domain-Driven Design helps create software that aligns with business needs.
  • Use aggregates to enforce consistency and encapsulate business rules.
  • Value objects represent domain concepts with no unique identity.
  • Abstract persistence with repositories for better maintainability.
  • Encapsulate cross-aggregate logic in domain services.

To deepen your knowledge, explore these resources:

  • The book Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans.
  • Implementing Domain-Driven Design by Vaughn Vernon.
  • Practice building a small project using DDD principles with C# and Entity Framework.

DDD is a journey, not a destination. Start small, involve your team, and continuously iterate. Happy coding!