Implementing Domain-Driven Design in C# .NET Core Projects

Domain-Driven Design (DDD) is a software development approach that focuses on understanding the problem domain to create a solution that meets the business's needs. DDD encourages creating a rich, expressive model that accurately reflects the domain.

DDD emphasizes creating a clean separation between the problem domain and the technical concerns. It consists of a set of patterns and principles that guide developers in designing software systems that are easy to understand, maintain, and evolve. Key benefits of DDD include:

  • Improved communication between the development team and domain experts
  • A more maintainable and scalable codebase
  • A focus on the essential complexity of the business domain

DDD Building Blocks

DDD is composed of several building blocks, including:

  • Entities: Classes that have a unique identity and encapsulate state and behavior
  • Value Objects: Immutable classes that encapsulate attributes but don't have an identity
  • Aggregates: A cluster of objects that are treated as a single unit
  • Repositories: Abstractions for accessing and persisting aggregates
  • Domain Events: Events that represent significant changes in the domain
  • Domain Services: Stateless services that perform domain-specific operations

Defining the Domain Model

Let's assume we're building an e-commerce application. Start by creating a Product entity and a Price value object:

public class Product
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public Price Price { get; private set; }

public Product(string name, Price price)
{
Id = Guid.NewGuid();
Name = name;
Price = price;
}
}

public class Price
{
public decimal Amount { get; }
public string Currency { get; }

public Price(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}

Implementing Aggregates and Aggregate Roots

In our example, an Order can be an aggregate root containing a collection of OrderLine entities:

public class Order
{
public Guid Id { get; private set; }
private List<OrderLine> _orderLines;
public IReadOnlyList<OrderLine> OrderLines => _orderLines.AsReadOnly();

public Order()
{
Id = Guid.NewGuid();
_orderLines = new List<OrderLine>();
}

public void AddOrderLine(Product product, int quantity)
{
_orderLines.Add(new OrderLine(product, quantity));
}
}

public class OrderLine
{
public Guid Id { get; private set; }
public Product Product { get; private set; }
public int Quantity { get; private set; }
}

public OrderLine(Product product, int quantity)
{
Id = Guid.NewGuid();
Product = product;
Quantity = quantity;
}

Repositories and Persistence

Create an interface for the `OrderRepository` and implement it using a persistence mechanism of your choice(e.g., Entity Framework Core):

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

public class OrderRepository : IOrderRepository
{
// Implementation using Entity Framework Core or another persistence mechanism
}

Implementing Domain Events

Define a domain event for when an order is placed:

public class OrderPlacedEvent
{
public Order Order { get; }

public OrderPlacedEvent(Order order)
{
Order = order;
}
}

Implement a simple event dispatcher:

public interface IDomainEventDispatcher
{
void Dispatch<TEvent>(TEvent domainEvent);
}

public class DomainEventDispatcher : IDomainEventDispatcher
{
public void Dispatch<TEvent>(TEvent domainEvent)
{
// Implementation to dispatch the event to appropriate event handlers
}
}

Raise the OrderPlacedEvent when an order is placed:

public class Order
{
// ...

public void PlaceOrder(IDomainEventDispatcher eventDispatcher)
{
// Business logic for placing an order
eventDispatcher.Dispatch(new OrderPlacedEvent(this));
}
}

Implementing Domain Services

Create a domain service to handle the process of creating and placing orders:

public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IDomainEventDispatcher _eventDispatcher;

public OrderService(IOrderRepository orderRepository, IDomainEventDispatcher eventDispatcher)
{
_orderRepository = orderRepository;
_eventDispatcher = eventDispatcher;
}

public async Task CreateAndPlaceOrderAsync(List<Product> products)
{
var order = new Order();

foreach (var product in products)
{
order.AddOrderLine(product, 1);
}

await _orderRepository.AddAsync(order);
order.PlaceOrder(_eventDispatcher);
}
}

By applying DDD principles, developers can build software systems that effectively tackle the complexity of the business domain while keeping the codebase maintainable and scalable.