Domain-Driven Design in ASP.NET Core Web API

Domain-Driven Design (DDD) in ASP.NET Core Web API

In this article, we will explore Domain-Driven Design (DDD) in the context of an ASP.NET Core Web API project, using SQL Server and the EF Core Code-First approach. Please read our previous article discussing Clean Architecture in ASP.NET Core Web API.

What Is Domain-Driven Design (DDD)?

Domain-Driven Design (DDD) is an approach to software development that focuses on modelling and deeply understanding the business domain. This ensures that the business logic is at the core of the software. DDD encourages collaboration between domain experts and developers to build rich, business-relevant models representing real-world concepts.

The goal is to encapsulate complex business rules within the domain model, ensuring that these rules govern the behavior of the system. This model becomes the heart of the application, and the entire system revolves around it. DDD aims to create maintainable, scalable, and flexible systems by aligning the technical architecture with business needs.

Real-time Example: Bookstore Management System

Imagine you are building a bookstore management system. In this system, concepts like Books, Authors, Orders, Customers, and Payments are all part of the business domain. In DDD:

  • You model each concept as a Domain Entity (e.g., a Book is an entity with an identity and behavior).
  • Business logic, such as calculating discounts or processing orders, is encapsulated in the domain model and not scattered throughout the application.
  • Developers and business experts share the system’s language and terminology, ensuring that everyone refers to the same thing in the same way.
Understanding Domain-Driven Design (DDD) Architecture:

For a better understanding of Domain-Driven Design (DDD) architecture, please have a look at the following diagram.

Understanding Domain-Driven Design (DDD) Architecture

Let us understand the above DDD architecture:

Presentation Layer (Controllers)

Purpose: This layer is responsible for handling incoming requests and managing user interactions. In a web application, this is typically where the API controllers reside. The following are the Key components:

  • Controllers: These are the entry points for external clients (users, other services, etc.). Controllers manage HTTP requests, handle input validation, and return appropriate responses to clients. In DDD, controllers are considered thin layers that delegate the actual work to the Application Layer.
  • Ubiquitous Language: The Ubiquitous Language is a key principle in DDD. It ensures that everyone, both developers and business stakeholders, uses the same terminology when discussing the domain. This eliminates ambiguity and provides better collaboration.
Application Layer (Services + DTOs)

Purpose: The Application Layer orchestrates the flow of data between the Domain Layer and the Presentation Layer. It coordinates business logic and workflows, typically without implementing domain logic itself. The following are the Key components:

  • Services: Services in the Application Layer act as facades that manage the system’s use cases. They contain application-specific logic, like how to handle a product’s lifecycle or how an order should be processed, but they delegate core business logic to the Domain Layer.
  • DTOs (Data Transfer Objects): DTOs are used to transfer data between layers (from the Application Layer to the Presentation Layer and vice versa). They are typically lightweight and exclude domain-specific behaviors, focusing only on the data necessary for the interaction.
Domain Layer (Entities, Value Objects, Repository Interfaces)

Purpose: The Domain Layer is the core of DDD and encapsulates the business logic and rules. This is where the true domain model resides, and it contains the entities and value objects that govern the behavior of the system. The following are the Key components:

  • Entities: Entities are business objects with a unique identity and lifecycle. They represent key business concepts in the system and typically include behaviors and state changes. Examples could include Product, Order, and Customer in an e-commerce system.
  • Value Objects: Value Objects represent aspects of the domain that do not have an identity but are defined by their attributes. They are immutable and typically used for attributes like Address or Money.
  • Repository Interfaces: Repositories provide access to domain entities and are used for data retrieval and persistence. These interfaces define the methods used by the Application Layer to interact with the Domain Layer.
Infrastructure Layer (EF Core DbContext)

Purpose: The Infrastructure Layer deals with external concerns like data access (through EF Core DbContext), third-party services, and integrations. It ensures that the Domain Layer remains decoupled from technical implementation details. The following are the Key components:

  • EF Core DbContext: The DbContext is part of the Infrastructure Layer and provides the mechanism for querying and saving data to the database. It manages the persistence of entities defined in the Domain Layer. For example, Product, Order, and Customer entities will be persisted in the database through EF Core.
  • Bounded Contexts: The concept of Bounded Contexts in DDD ensures that each subdomain (e.g., Product Management, Order Processing) has its own model and is isolated from other parts of the system. The Infrastructure Layer helps implement the Bounded Contexts by managing persistence mechanisms and database interactions within each context.
How These Layers Work Together
  • Presentation Layer: The external interface of the application (controllers) interacts with the Application Layer to request specific use cases. Controllers typically only focus on routing and formatting data.
  • Application Layer: The Application Layer coordinates the execution of use cases by using services and DTOs to transfer data. It interacts with the Domain Layer to execute business rules and workflows and then returns the results to the Presentation Layer.
  • Domain Layer: The Domain Layer contains the Entities and Value Objects that hold the business logic. This layer is independent of other technical concerns and is responsible for ensuring the correctness of the system’s behavior.
  • Infrastructure Layer: The Infrastructure Layer provides support for technical concerns like data persistence (via EF Core DbContext) and manages interactions with external systems. It ensures that the business logic in the Domain Layer remains decoupled from the database and other external systems.
Why DDD Matters in ASP.NET Core Web API:

Use Case: User Registration: Our system needs to handle user registration. The registration process involves creating a user, validating their email address, and saving the data to a database.

Problem Without DDD in ASP.NET Core Web API:

In a non-DDD system, everything might be mixed together in the controller or service, including business logic, validation, and data access. The controller is responsible for validating the email, checking if the email already exists, and saving the user to the database. The controller is handling both application logic and infrastructure concerns.

public class UserController : ControllerBase
{
    private readonly ApplicationDbContext _context;

    public UserController(ApplicationDbContext context)
    {
        _context = context;
    }

    // POST: api/register
    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] User user)
    {
        // Business logic scattered here
        if (!user.Email.Contains("@"))
        {
            return BadRequest("Invalid email format.");
        }

        // Check if the email already exists
        if (_context.Users.Any(u => u.Email == user.Email))
        {
            return Conflict("Email already in use.");
        }

        _context.Users.Add(user);
        await _context.SaveChangesAsync();

        return CreatedAtAction("GetUser", new { id = user.Id }, user);
    }
}
Problems with this Approach:
  • Scattered Logic: Business logic, such as email validation and checking for duplicate emails, is mixed with the data access logic (e.g., querying the database in the controller).
  • Hard to maintain: If the email validation rule changes (e.g., allowing special characters), you need to update the controller and potentially other places.
  • Tightly Coupled: The controller depends heavily on the EF Core database and business logic, making it hard to test or swap out different data access mechanisms.
  • Lack of Separation: The controller should be responsible for handling HTTP requests, not business logic.
Solution with DDD:

In a DDD approach, we separate the business logic, data access, and presentation concerns. We create a domain model that represents the user registration process and encapsulates the business rules.

Domain Layer: User Entity

The User entity in the Domain Layer contains the business rules. For example, it could contain logic for validating the email.

public class User
{
    public int Id { get; set; }
    public string Email { get; set; }

    // Business rule: Ensure that the email is valid
    public void ValidateEmail()
    {
        if (string.IsNullOrWhiteSpace(Email) || !Email.Contains("@"))
            throw new InvalidOperationException("Invalid email format.");
    }
}
Repository Interface: Abstracts data access logic
public interface IUserRepository
{
    Task<User> GetByEmailAsync(string email);
    Task AddAsync(User user);
}
Application Layer: UserService

The UserService in the Application Layer orchestrates the registration process. It uses the User entity and IUserRepository to manage the user registration workflow.

public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task RegisterAsync(User user)
    {
        // Business logic is here
        user.ValidateEmail();

        // Check if the email already exists
        var existingUser = await _userRepository.GetByEmailAsync(user.Email);
        if (existingUser != null)
        {
            throw new InvalidOperationException("Email already in use.");
        }

        // Save to repository
        await _userRepository.AddAsync(user);
    }
}
Infrastructure Layer: Repository Implementation

The Infrastructure Layer is responsible for the actual data access, but it doesn’t contain any business logic. It simply implements the repository interface to interact with the database.

public class UserRepository : IUserRepository
{
    private readonly ApplicationDbContext _context;

    public UserRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<User> GetByEmailAsync(string email)
    {
        return await _context.Users.SingleOrDefaultAsync(u => u.Email == email);
    }

    public async Task AddAsync(User user)
    {
        await _context.Users.AddAsync(user);
        await _context.SaveChangesAsync();
    }
}
Presentation Layer: UserController

The UserController now only focuses on handling HTTP requests and interacting with the UserService. It doesn’t handle business logic directly.

public class UserController : ControllerBase
{
    private readonly UserService _userService;

    public UserController(UserService userService)
    {
        _userService = userService;
    }

    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] User user)
    {
        try
        {
            await _userService.RegisterAsync(user);
            return CreatedAtAction("GetUser", new { id = user.Id }, user);
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(ex.Message);
        }
    }
}
Points to Remember:
  • Without DDD, Business logic and data access are tangled together in the controller, leading to maintenance challenges and tight coupling.
  • With DDD, Business logic is encapsulated in the domain model, data access is abstracted in repositories, and the controller simply coordinates the flow. This makes the codebase easier to maintain, test, and evolve.
Real-time E-commerce Application using Domain-Driven Design (DDD):

Let us implement the real-time E-commerce Application using Domain-Driven Design (DDD) with ASP.NET Core Web API, SQL Server, and EF Core Code-First Approach. We will break down the structure of the application into domain concepts, explain the DDD principles we are using.

Step 1: Set up Project Structure

We will create the following project structure based on the DDD approach:

  • Domain Layer: Contains core business logic, entities, value objects, aggregates, and repositories.
  • Application Layer: Coordinates operations and contains service classes.
  • Infrastructure Layer: This layer handles external dependencies like the database (SQL Server), repositories, and EF Core configurations.
  • Presentation Layer: Contains API controllers for HTTP requests.

Open Visual Studio and create a new ASP.NET Core Web API project called ECommerceAPI. We will use Entity Framework Core with SQL Server, so we need to install the necessary packages. Please execute the following commands in Visual Studio Command Prompt.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Step 2: Define Domain Layer

In DDD, the Domain Layer encapsulates the core business logic and is the heart of the application. It’s the core of your application where the primary business concepts are defined. It contains Entities, Value Objects, Aggregates, and Repositories interfaces.

Creating Entities:

Entities represent the core business objects in DDD. They are defined by their identity and can have complex behaviors and state changes. Entities ensure that all business rules are encapsulated within the domain. First, create a folder named Domain in the project root directory. Then, inside the Domain folder, create a subfolder named Entities where we will create all our Entities.

Product Entity

Create a class file named Product.cs within the Domain/Entities folder and then copy and paste the following code. This class defines the Product entity in the domain, representing the core business logic for products in the application, including properties like Id, Name, Price, and StockQuantity. It also contains a business rule that ensures the product’s price is greater than zero.

using System.ComponentModel.DataAnnotations.Schema;

namespace ECommerceAPI.Domain.Entities
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        [Column(TypeName ="decimal(12,2)")]
        public decimal Price { get; set; }
        public int StockQuantity { get; set; }

        // Business rule: A product's price must be greater than 0.
        public void ValidatePrice()
        {
            if (Price <= 0)
                throw new InvalidOperationException("Price must be greater than zero.");
        }
    }
}
Order Entity

Create a class file named Order.cs within the Domain/Entities folder and then copy and paste the following code. This class defines the Order entity, which represents a customer’s order in the e-commerce system. It includes properties such as Id, OrderDate, CustomerId, TotalAmount, and a list of OrderItems. The RecalculateTotal method ensures the total amount is updated based on the order’s items.

using System.ComponentModel.DataAnnotations.Schema;

namespace ECommerceAPI.Domain.Entities
{
    public class Order
    {
        public int Id { get; set; }
        public DateTime OrderDate { get; set; }
        public int CustomerId { get; set; }
        public Customer Customer { get; set; }
        public List<OrderItem> OrderItems { get; set; }
        [Column(TypeName = "decimal(12,2)")]
        public decimal TotalAmount { get; set; }

        // Business rule: Order total should be recalculated based on OrderItems
        public void RecalculateTotal()
        {
            TotalAmount = OrderItems.Sum(item => item.TotalPrice);
        }
    }
}
Customer Entity

Create a class file named Customer.cs within the Domain/Entities folder and then copy and paste the following code. This class represents the Customer entity, encapsulating customer-related data, including ID, Name, and Email. It also includes a business rule to validate the customer’s email.

namespace ECommerceAPI.Domain.Entities
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public string Email { get; set; } = null!;

        // Business rule: Ensure that the email is valid
        public void ValidateEmail()
        {
            if (string.IsNullOrWhiteSpace(Email) || !Email.Contains('@'))
                throw new InvalidOperationException("Invalid email address.");
        }
    }
}
OrderItem Entity

Create a class file named OrderItem.cs within the Domain/Entities folder and then copy and paste the following code. The OrderItem entity represents a single item within an order, containing Id, ProductId, Quantity, Price, and a calculated property, TotalPrice (Quantity * Price).

using System.ComponentModel.DataAnnotations.Schema;

namespace ECommerceAPI.Domain.Entities
{
    public class OrderItem
    {
        public int Id { get; set; }
        public int ProductId { get; set; }
        public Product Product { get; set; }
        public int Quantity { get; set; }
        [Column(TypeName = "decimal(12,2)")]
        public decimal Price { get; set; }

        public decimal TotalPrice => Quantity * Price;
    }
}
Role of Entities in DDD:

Product, Order, Customer, and OrderItem are examples of Entities in our application. They represent key business concepts, each having an identity and encapsulating business rules.

  • The Product entity contains properties like ID, Name, Price, and StockQuantity. It also has a business rule that the price must be greater than zero.
  • The Order entity contains an OrderDate, CustomerId, and a list of OrderItems. It also has a business rule that ensures the total order amount is recalculated based on the items.
  • Customer contains customer-specific data such as ID, Name, and Email. It has a business rule to validate the email format.
  • OrderItem represents a line item in an order, linking a Product to an order with a quantity and price.

These entities represent the primary concepts in the domain and encapsulate business logic that ensures the system operates according to business rules. These entities are persistent (stored in the database) but also contain methods that encapsulate business logic.

Define Repositories Interfaces

Repository interfaces define the contract for interacting with domain entities. They provide methods for retrieving, adding, updating, and deleting entities in a decoupled manner, abstracting the underlying persistence logic. Repositories ensure the Domain Layer remains independent of the data access layer. First, create a folder named Interfaces within the Domain folder. Inside this Domain/Interfaces, we will create all our Repository interfaces for the Product, Order, and Customer entities.

IProductRepository

Create a class file named IProductRepository.cs within the Domain/Interfaces folder and then copy and paste the following code. This interface defines CRUD operations for Product: fetching by ID or all products, and methods to add, update, or delete. Application services depend on it for persistence without using EF Core directly.

using ECommerceAPI.Domain.Entities;

namespace ECommerceAPI.Domain.Interfaces
{
    public interface IProductRepository
    {
        Task<Product?> GetByIdAsync(int id);
        Task<IEnumerable<Product>> GetAllAsync();
        Task AddAsync(Product product);
        Task UpdateAsync(Product product);
        Task DeleteAsync(int id);
    }
}
IOrderRepository

Create a class file named IOrderRepository.cs within the Domain/Interfaces folder and then copy and paste the following code. The IOrderRepository declares methods to retrieve and manipulate Order aggregates (GetById, GetAll, Add, Update, Delete). Domain services and application services use it to load and save orders.

using ECommerceAPI.Domain.Entities;

namespace ECommerceAPI.Domain.Interfaces
{
    public interface IOrderRepository
    {
        Task<Order?> GetByIdAsync(int id);
        Task<IEnumerable<Order>> GetAllAsync();
        Task AddAsync(Order order);
        Task UpdateAsync(Order order);
        Task DeleteAsync(int id);
    }
}
ICustomerRepository

Create a class file named ICustomerRepository.cs within the Domain/Interfaces folder and then copy and paste the following code. This interface outlines persistence operations for Customer: retrieval, listing, creation, modification, and removal. Application services for customers rely on it to implement use cases.

using ECommerceAPI.Domain.Entities;

namespace ECommerceAPI.Domain.Interfaces
{
    public interface ICustomerRepository
    {
        Task<Customer?> GetByIdAsync(int id);
        Task<IEnumerable<Customer>> GetAllAsync();
        Task AddAsync(Customer customer);
        Task UpdateAsync(Customer customer);
        Task DeleteAsync(int id);
    }
}
Role of Repositories in DDD:
  • IProductRepository, IOrderRepository, and ICustomerRepository are examples of repositories in DDD. They provide access to the database entities and abstract away the data persistence logic.
  • Repositories act as the gatekeepers for the domain model. Instead of dealing directly with the database, your application interacts with these repositories to retrieve and store domain entities.

Purpose in DDD: The Domain Layer‘s primary objective is to model the business rules using Entities and Value Objects, ensuring the business logic is decoupled from external systems and technicalities.

Step 3: Define Application Layer

In DDD, the Application Layer defines the application’s logic and workflows, but it does not contain business rules. Instead, it interacts with the Domain Layer to implement specific use cases and manages the flow of data between the Domain Layer and Presentation Layer (APIs, user interfaces). First, create a folder named Application in the project root directory.

DTOs for Product, Order, and Customer

Data Transfer Objects (DTOs) are lightweight objects used to transfer data between the client and the server. DTOs help shape the data for specific use cases, often containing only the necessary fields and preventing the exposure of sensitive or unnecessary information. First, create a folder named DTOs within the Application folder. Inside this Application/DTOs, we will create all our DTOs for Customer, Product, and Order.

ProductDTO

Create a class file named ProductDTO.cs within the Application/DTOs folder and then copy and paste the following code. The ProductDTO is a data-transfer object used by controllers to expose or accept product data over HTTP. It mirrors Product properties but contains no business logic.

namespace ECommerceAPI.Application.DTOs
{
    public class ProductDTO
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public decimal Price { get; set; }
        public int StockQuantity { get; set; }
    }
}
OrderItemDTO

Create a class file named OrderItemDTO.cs within the Application/DTOs folder and then copy and paste the following code. This DTO represents the data shape for an OrderItem in API requests/responses: it contains Id, ProductId, Quantity, Price, and a computed TotalPrice. Controllers and services use it to exchange order-line information.

namespace ECommerceAPI.Application.DTOs
{
    public class OrderItemDTO
    {
        public int Id { get; set; }
        public int ProductId { get; set; }
        public int Quantity { get; set; }
        public decimal Price { get; set; }
        public decimal TotalPrice => Quantity * Price;
    }
}
OrderDTO

Create a class file named OrderDTO.cs within the Application/DTOs folder and then copy and paste the following code. The OrderDTO packages order data for API clients, including ID, OrderDate, CustomerId, a list of OrderItemDTOs, and the total amount. Application services return it after performing domain operations.

namespace ECommerceAPI.Application.DTOs
{
    public class OrderDTO
    {
        public int Id { get; set; }
        public DateTime OrderDate { get; set; }
        public int CustomerId { get; set; }
        public List<OrderItemDTO> OrderItems { get; set; }
        public decimal TotalAmount { get; set; }
    }
}
CustomerDTO

Create a class file named CustomerDTO.cs within the Application/DTOs folder and then copy and paste the following code. This DTO carries customer details (ID, Name, Email) to and from API endpoints. It’s used in controller methods for customer retrieval, creation, and updates.

namespace ECommerceAPI.Application.DTOs
{
    public class CustomerDTO
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public string Email { get; set; } = null!;
    }
}
Role of DTOs (Data Transfer Objects) in DDD:

ProductDTO, OrderDTO, CustomerDTO, and OrderItemDTO are DTOs in our example. DTOs serve as data containers for transferring data between the Application Layer and the Presentation Layer. They are typically lighter representations of the domain entities, often excluding sensitive data or details not needed by the client. In our case:

  • ProductDTO contains only the essential fields for a product (Id, Name, Price, StockQuantity).
  • OrderDTO contains the necessary fields for orders, including related OrderItemDTOs.
  • CustomerDTO simplifies the Customer entity for the Presentation Layer.
Creating Services:

Services represent the business logic layer and manage the interaction between the domain and other application layers. They are responsible for implementing business rules and handling the application’s workflows. Services act as intermediaries between the Application Layer and the Domain Layer, ensuring business logic is executed. First, create a subfolder named Services within the Application folder, where we will create all our application-layer Services.

ProductService

Create a class file named ProductService.cs within the Application/Services folder and then copy and paste the following code. The ProductService in the Application Layer orchestrates use cases: listing products, retrieving by ID, and delegating create/update/delete operations to the repository, including invoking ValidatePrice() on the domain entity.

using ECommerceAPI.Application.DTOs;
using ECommerceAPI.Domain.Entities;
using ECommerceAPI.Domain.Interfaces;

namespace ECommerceAPI.Application.Services
{
    public class ProductService
    {
        private readonly IProductRepository _productRepository;

        public ProductService(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }

        // Get all products
        public async Task<IEnumerable<ProductDTO>> GetAllAsync()
        {
            var products = await _productRepository.GetAllAsync();
            return products.Select(p => new ProductDTO
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price,
                StockQuantity = p.StockQuantity
            }).ToList();
        }

        // Get a product by ID
        public async Task<ProductDTO?> GetByIdAsync(int id)
        {
            var product = await _productRepository.GetByIdAsync(id);
            if (product == null)
                return null;

            return new ProductDTO
            {
                Id = product.Id,
                Name = product.Name,
                Price = product.Price,
                StockQuantity = product.StockQuantity
            };
        }

        // Add a new product
        public async Task AddProductAsync(ProductDTO productDto)
        {
            var product = new Product
            {
                Name = productDto.Name,
                Price = productDto.Price,
                StockQuantity = productDto.StockQuantity
            };

            product.ValidatePrice();  // Business rule validation
            await _productRepository.AddAsync(product);
        }

        // Update an existing product
        public async Task UpdateProductAsync(ProductDTO productDto)
        {
            var product = await _productRepository.GetByIdAsync(productDto.Id);
            if (product == null)
                throw new Exception("Product not found.");

            product.Name = productDto.Name;
            product.Price = productDto.Price;
            product.StockQuantity = productDto.StockQuantity;

            product.ValidatePrice();  // Business rule validation
            await _productRepository.UpdateAsync(product);
        }

        // Delete a product
        public async Task DeleteProductAsync(int id)
        {
            await _productRepository.DeleteAsync(id);
        }
    }
}
OrderService

Create a class file named OrderService.cs within the Application/Services folder and then copy and paste the following code. This service handles order-related scenarios: listing, retrieving, creating, updating, and deleting orders. It constructs or loads Order aggregates, invokes RecalculateTotal(), and persists via IOrderRepository.

using ECommerceAPI.Application.DTOs;
using ECommerceAPI.Domain.Entities;
using ECommerceAPI.Domain.Interfaces;

namespace ECommerceAPI.Application.Services
{
    public class OrderService
    {
        private readonly IOrderRepository _orderRepository;

        public OrderService(IOrderRepository orderRepository)
        {
            _orderRepository = orderRepository;
        }

        // Get all orders
        public async Task<IEnumerable<OrderDTO>> GetAllAsync()
        {
            var orders = await _orderRepository.GetAllAsync();
            return orders.Select(o => new OrderDTO
            {
                Id = o.Id,
                OrderDate = o.OrderDate,
                CustomerId = o.CustomerId,
                OrderItems = o.OrderItems.Select(oi => new OrderItemDTO
                {
                    Id = oi.Id,
                    ProductId = oi.ProductId,
                    Quantity = oi.Quantity,
                    Price = oi.Price
                }).ToList(),
                TotalAmount = o.TotalAmount
            }).ToList();
        }

        // Get order by ID
        public async Task<OrderDTO?> GetByIdAsync(int id)
        {
            var order = await _orderRepository.GetByIdAsync(id);
            if (order == null)
                return null;

            return new OrderDTO
            {
                Id = order.Id,
                OrderDate = order.OrderDate,
                CustomerId = order.CustomerId,
                OrderItems = order.OrderItems.Select(oi => new OrderItemDTO
                {
                    Id = oi.Id,
                    ProductId = oi.ProductId,
                    Quantity = oi.Quantity,
                    Price = oi.Price
                }).ToList(),
                TotalAmount = order.TotalAmount
            };
        }

        // Create a new order
        public async Task CreateOrderAsync(OrderDTO orderDto)
        {
            var order = new Order
            {
                OrderDate = orderDto.OrderDate,
                CustomerId = orderDto.CustomerId,
                OrderItems = orderDto.OrderItems.Select(oi => new OrderItem
                {
                    ProductId = oi.ProductId,
                    Quantity = oi.Quantity,
                    Price = oi.Price
                }).ToList()
            };

            order.RecalculateTotal();  // Business rule to calculate total
            await _orderRepository.AddAsync(order);
        }

        // Update an existing order
        public async Task UpdateOrderAsync(OrderDTO orderDto)
        {
            var order = await _orderRepository.GetByIdAsync(orderDto.Id);
            if (order == null)
                throw new Exception("Order not found.");

            order.OrderDate = orderDto.OrderDate;
            order.CustomerId = orderDto.CustomerId;
            order.OrderItems = orderDto.OrderItems.Select(oi => new OrderItem
            {
                ProductId = oi.ProductId,
                Quantity = oi.Quantity,
                Price = oi.Price
            }).ToList();

            order.RecalculateTotal();  // Business rule to recalculate total
            await _orderRepository.UpdateAsync(order);
        }

        // Delete an order
        public async Task DeleteOrderAsync(int id)
        {
            await _orderRepository.DeleteAsync(id);
        }
    }
}
CustomerService

Create a class file named CustomerService.cs within the Application/Services folder and then copy and paste the following code. The CustomerService offers customer use cases: listing, retrieval, creation (with ValidateEmail()), updates, and deletion. It maps between CustomerDTO and Customer entities and calls the repository to persist changes.

using ECommerceAPI.Application.DTOs;
using ECommerceAPI.Domain.Entities;
using ECommerceAPI.Domain.Interfaces;

namespace ECommerceAPI.Application.Services
{
    public class CustomerService
    {
        private readonly ICustomerRepository _customerRepository;

        public CustomerService(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }

        // Get all customers
        public async Task<IEnumerable<CustomerDTO>> GetAllAsync()
        {
            var customers = await _customerRepository.GetAllAsync();
            return customers.Select(c => new CustomerDTO
            {
                Id = c.Id,
                Name = c.Name,
                Email = c.Email
            }).ToList();
        }

        // Get customer by ID
        public async Task<CustomerDTO?> GetByIdAsync(int id)
        {
            var customer = await _customerRepository.GetByIdAsync(id);
            if (customer == null)
                return null;

            return new CustomerDTO
            {
                Id = customer.Id,
                Name = customer.Name,
                Email = customer.Email
            };
        }

        // Add a new customer
        public async Task AddCustomerAsync(CustomerDTO customerDto)
        {
            var customer = new Customer
            {
                Name = customerDto.Name,
                Email = customerDto.Email
            };

            customer.ValidateEmail();  // Business rule validation
            await _customerRepository.AddAsync(customer);
        }

        // Update an existing customer
        public async Task UpdateCustomerAsync(CustomerDTO customerDto)
        {
            var customer = await _customerRepository.GetByIdAsync(customerDto.Id);
            if (customer == null)
                throw new Exception("Customer not found.");

            customer.Name = customerDto.Name;
            customer.Email = customerDto.Email;
            customer.ValidateEmail();  // Business rule validation
            await _customerRepository.UpdateAsync(customer);
        }

        // Delete a customer
        public async Task DeleteCustomerAsync(int id)
        {
            await _customerRepository.DeleteAsync(id);
        }
    }
}
Services in DDD:

ProductService, OrderService, and CustomerService represent the application services. These services implement the application’s use cases. For example, the ProductService handles logic such as adding a product, updating product details, and retrieving products from the repository.

Services interact with repositories to retrieve and persist entities and often perform business logic (in this case, using methods like ValidatePrice() or RecalculateTotal()).

Purpose in DDD: The Application Layer’s main role is to handle use cases, coordinate operations, and interact with the Domain Layer to ensure that the required business rules and logic are executed. The Application Layer should not contain business logic itself, but orchestrate the business logic exposed by the Domain Layer.

Step 4: Define Infrastructure Layer

In DDD, the Infrastructure Layer manages technical concerns, such as the database, third-party services, and other external systems, while ensuring the Domain Layer remains decoupled from these concerns. First, create a folder named Infrastructure in the project root directory.

ApplicationDbContext

Create a subfolder named Data within the Infrastructure folder. Then, create a class file named ApplicationDbContext.cs within the Infrastructure/Data folder and then copy and paste the following code. This EF Core DbContext class defines DbSet<T> properties for each entity (Product, Order, Customer, OrderItem), configures the model, and seeds initial data. It underpins your database schema and migrations.

using ECommerceAPI.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace ECommerceAPI.Infrastructure.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

        public DbSet<Product> Products { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<Customer> Customers { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }

        // Seed data for initial products and customers
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Seed Customers
            modelBuilder.Entity<Customer>().HasData(
                new Customer { Id = 1, Name = "John Doe", Email = "john.doe@example.com" },
                new Customer { Id = 2, Name = "Jane Smith", Email = "jane.smith@example.com" },
                new Customer { Id = 3, Name = "Alice Johnson", Email = "alice.johnson@example.com" }
            );

            // Seed Products
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 1, Name = "Laptop", Price = 1200.00m, StockQuantity = 50 },
                new Product { Id = 2, Name = "Smartphone", Price = 800.00m, StockQuantity = 100 },
                new Product { Id = 3, Name = "Headphones", Price = 150.00m, StockQuantity = 200 },
                new Product { Id = 4, Name = "Monitor", Price = 300.00m, StockQuantity = 30 }
            );
        }
    }
}
Implementing Repositories

These are concrete classes that implement the repository interfaces defined in the Domain Layer. They interact with the database (using EF Core) to retrieve and store domain entities. Repositories encapsulate the persistence logic, ensuring that the Domain Layer does not directly depend on the data source. First, create a folder named Repositories within the Infrastructure folder. Inside this Infrastructure/Repositories, we will create all our Repository classes.

ProductRepository

Create a class file named ProductRepository.cs within the Infrastructure/Repositories folder and then copy and paste the following code. This concrete class implements IProductRepository using ApplicationDbContext. It translates repository methods into EF Core queries (FindAsync, ToListAsync, AddAsync, Update, SaveChanges).

using ECommerceAPI.Domain.Entities;
using ECommerceAPI.Domain.Interfaces;
using ECommerceAPI.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace ECommerceAPI.Infrastructure.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly ApplicationDbContext _context;

        public ProductRepository(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<Product> GetByIdAsync(int id)
        {
            return await _context.Products.FindAsync(id);
        }

        public async Task<IEnumerable<Product>> GetAllAsync()
        {
            return await _context.Products.ToListAsync();
        }

        public async Task AddAsync(Product product)
        {
            await _context.Products.AddAsync(product);
            await _context.SaveChangesAsync();
        }

        public async Task UpdateAsync(Product product)
        {
            _context.Products.Update(product);
            await _context.SaveChangesAsync();
        }

        public async Task DeleteAsync(int id)
        {
            var product = await _context.Products.FindAsync(id);
            if (product != null)
            {
                _context.Products.Remove(product);
                await _context.SaveChangesAsync();
            }
        }
    }
}
OrderRepository

Create a class file named OrderRepository.cs within the Infrastructure/Repositories folder and then copy and paste the following code. The OrderRepository implements IOrderRepository by querying the ApplicationDbContext.Orders (including related OrderItem and Product), and handling add/update/delete via EF Core.

using ECommerceAPI.Domain.Entities;
using ECommerceAPI.Domain.Interfaces;
using ECommerceAPI.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace ECommerceAPI.Infrastructure.Repositories
{
    public class OrderRepository : IOrderRepository
    {
        private readonly ApplicationDbContext _context;

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

        public async Task<Order> GetByIdAsync(int id)
        {
            return await _context.Orders
                .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
                .FirstOrDefaultAsync(o => o.Id == id);
        }

        public async Task<IEnumerable<Order>> GetAllAsync()
        {
            return await _context.Orders
                .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
                .ToListAsync();
        }

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

        public async Task UpdateAsync(Order order)
        {
            _context.Orders.Update(order);
            await _context.SaveChangesAsync();
        }

        public async Task DeleteAsync(int id)
        {
            var order = await _context.Orders.FindAsync(id);
            if (order != null)
            {
                _context.Orders.Remove(order);
                await _context.SaveChangesAsync();
            }
        }
    }
}
CustomerRepository

Create a class file named CustomerRepository.cs within the Infrastructure/Repositories folder and then copy and paste the following code. This class implements the ICustomerRepository against the ApplicationDbContext.Customers set, providing asynchronous CRUD operations and ensuring referential data (if any) is loaded or saved.

using ECommerceAPI.Domain.Entities;
using ECommerceAPI.Domain.Interfaces;
using ECommerceAPI.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace ECommerceAPI.Infrastructure.Repositories
{
    public class CustomerRepository : ICustomerRepository
    {
        private readonly ApplicationDbContext _context;

        public CustomerRepository(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<Customer> GetByIdAsync(int id)
        {
            return await _context.Customers.FindAsync(id);
        }

        public async Task<IEnumerable<Customer>> GetAllAsync()
        {
            return await _context.Customers.ToListAsync();
        }

        public async Task AddAsync(Customer customer)
        {
            await _context.Customers.AddAsync(customer);
            await _context.SaveChangesAsync();
        }

        public async Task UpdateAsync(Customer customer)
        {
            _context.Customers.Update(customer);
            await _context.SaveChangesAsync();
        }

        public async Task DeleteAsync(int id)
        {
            var customer = await _context.Customers.FindAsync(id);
            if (customer != null)
            {
                _context.Customers.Remove(customer);
                await _context.SaveChangesAsync();
            }
        }
    }
}
Repositories Implementations in DDD:
  • The ProductRepository, OrderRepository, and CustomerRepository classes in the Infrastructure Layer implement the repository interfaces defined in the Domain Layer.
  • These repositories handle the EF Core logic to interact with the SQL Server database (e.g., AddAsync, GetByIdAsync, UpdateAsync, DeleteAsync).

Purpose in DDD: The Infrastructure Layer is responsible for handling data persistence and external systems. It implements the repositories defined in the Domain Layer to interact with the database, but should not contain business logic.

Step 5: Define Presentation Layer (API Controllers)

The Presentation Layer in DDD represents the user interface (in this case, API Controllers) that exposes the functionality to the outside world. It is responsible for handling user requests, validating input, and returning appropriate responses.

Creating Controllers:

Controllers in the Presentation Layer handle incoming HTTP requests, perform input validation, interact with the Application Layer to execute the necessary logic, and return responses. They are responsible for exposing API endpoints and managing the client’s interaction with the backend system.

ProductController

Create an API Empty Controller named ProductController within the Controllers folder and then copy and paste the following code. The following API controller exposes REST endpoints (GET, POST, PUT, DELETE) for products.

using ECommerceAPI.Application.DTOs;
using ECommerceAPI.Application.Services;
using Microsoft.AspNetCore.Mvc;

namespace ECommerceAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductController : ControllerBase
    {
        private readonly ProductService _productService;

        public ProductController(ProductService productService)
        {
            _productService = productService;
        }

        // GET: api/products
        [HttpGet]
        public async Task<ActionResult<IEnumerable<ProductDTO>>> GetAll()
        {
            var products = await _productService.GetAllAsync();
            return Ok(products);
        }

        // GET: api/products/{id}
        [HttpGet("{id}")]
        public async Task<ActionResult<ProductDTO>> GetById(int id)
        {
            var product = await _productService.GetByIdAsync(id);
            if (product == null)
                return NotFound();

            return Ok(product);
        }

        // POST: api/products
        [HttpPost]
        public async Task<IActionResult> Create([FromBody] ProductDTO productDto)
        {
            await _productService.AddProductAsync(productDto);
            return CreatedAtAction(nameof(GetById), new { id = productDto.Id }, productDto);
        }

        // PUT: api/products/{id}
        [HttpPut("{id}")]
        public async Task<IActionResult> Update(int id, [FromBody] ProductDTO productDto)
        {
            if (id != productDto.Id)
                return BadRequest();

            await _productService.UpdateProductAsync(productDto);
            return NoContent();
        }

        // DELETE: api/products/{id}
        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            await _productService.DeleteProductAsync(id);
            return NoContent();
        }
    }
}
OrderController

Create an API Empty Controller named OrderController within the Controllers folder and then copy and paste the following code. This controller exposes REST endpoints for orders, using OrderService to perform use cases. It handles request validation, route mapping, and status codes and returns OrderDTO results.

using ECommerceAPI.Application.DTOs;
using ECommerceAPI.Application.Services;
using Microsoft.AspNetCore.Mvc;

namespace ECommerceAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrderController : ControllerBase
    {
        private readonly OrderService _orderService;

        public OrderController(OrderService orderService)
        {
            _orderService = orderService;
        }

        // GET: api/orders
        [HttpGet]
        public async Task<ActionResult<IEnumerable<OrderDTO>>> GetAll()
        {
            var orders = await _orderService.GetAllAsync();
            return Ok(orders);
        }

        // GET: api/orders/{id}
        [HttpGet("{id}")]
        public async Task<ActionResult<OrderDTO>> GetById(int id)
        {
            var order = await _orderService.GetByIdAsync(id);
            if (order == null)
                return NotFound();

            return Ok(order);
        }

        // POST: api/orders
        [HttpPost]
        public async Task<IActionResult> Create([FromBody] OrderDTO orderDto)
        {
            await _orderService.CreateOrderAsync(orderDto);
            return CreatedAtAction(nameof(GetById), new { id = orderDto.Id }, orderDto);
        }

        // PUT: api/orders/{id}
        [HttpPut("{id}")]
        public async Task<IActionResult> Update(int id, [FromBody] OrderDTO orderDto)
        {
            if (id != orderDto.Id)
                return BadRequest();

            await _orderService.UpdateOrderAsync(orderDto);
            return NoContent();
        }

        // DELETE: api/orders/{id}
        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            await _orderService.DeleteOrderAsync(id);
            return NoContent();
        }
    }
}
CustomerController

Create an API Empty Controller named CustomerController within the Controllers folder and then copy and paste the following code. The following API controller manages RESTful endpoints for customers by calling CustomerService. It processes incoming CustomerDTOs, handles not-found cases, and returns standard HTTP responses.

using ECommerceAPI.Application.DTOs;
using ECommerceAPI.Application.Services;
using Microsoft.AspNetCore.Mvc;

namespace ECommerceAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomerController : ControllerBase
    {
        private readonly CustomerService _customerService;

        public CustomerController(CustomerService customerService)
        {
            _customerService = customerService;
        }

        // GET: api/customers
        [HttpGet]
        public async Task<ActionResult<IEnumerable<CustomerDTO>>> GetAll()
        {
            var customers = await _customerService.GetAllAsync();
            return Ok(customers);
        }

        // GET: api/customers/{id}
        [HttpGet("{id}")]
        public async Task<ActionResult<CustomerDTO>> GetById(int id)
        {
            var customer = await _customerService.GetByIdAsync(id);
            if (customer == null)
                return NotFound();

            return Ok(customer);
        }

        // POST: api/customers
        [HttpPost]
        public async Task<IActionResult> Create([FromBody] CustomerDTO customerDto)
        {
            await _customerService.AddCustomerAsync(customerDto);
            return CreatedAtAction(nameof(GetById), new { id = customerDto.Id }, customerDto);
        }

        // PUT: api/customers/{id}
        [HttpPut("{id}")]
        public async Task<IActionResult> Update(int id, [FromBody] CustomerDTO customerDto)
        {
            if (id != customerDto.Id)
                return BadRequest();

            await _customerService.UpdateCustomerAsync(customerDto);
            return NoContent();
        }

        // DELETE: api/customers/{id}
        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            await _customerService.DeleteCustomerAsync(id);
            return NoContent();
        }
    }
}
Controllers in DDD:
  • The ProductController, OrderController, and CustomerController are API controllers that expose HTTP endpoints to interact with the Application Layer.
  • These controllers receive DTOs from the client, pass them to the corresponding Service, and return the results to the client.
  • Controllers in DDD are thin layers that simply delegate the work to the Application Layer (Services).

Purpose in DDD: The Presentation Layer should handle user interactions and transfer data to and from the Application Layer (via DTOs). It should not contain any business logic or deep domain knowledge.

appsettings.json File

This file contains the application settings, including the database connection string. So, please modify the appsettings.json file as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Set up Dependency Injection and Database Configuration

In the Program.cs, configure the necessary services for Dependency Injection.

using ECommerceAPI.Application.Services;
using ECommerceAPI.Domain.Interfaces;
using ECommerceAPI.Infrastructure.Data;
using ECommerceAPI.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;

namespace ECommerceAPI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            // Optionally, configure JSON options or other formatter settings
            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // Configure JSON serializer settings to keep the Original names in serialization and deserialization
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Add DbContext with SQL Server
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            // Register repositories
            builder.Services.AddScoped<IProductRepository, ProductRepository>();
            builder.Services.AddScoped<IOrderRepository, OrderRepository>();
            builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();

            // Register services
            builder.Services.AddScoped<ProductService>();
            builder.Services.AddScoped<OrderService>();
            builder.Services.AddScoped<CustomerService>();

            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Creating and Applying Database Migration:

In Visual Studio, open the Package Manager Console and execute the Add-Migration and Update-Database commands as follows to generate the Migration file and then apply the Migration file to create the ECommerceDB database and required tables:

Creating and Applying Database Migration

Once you execute the above commands, verify the database, you should see the ECommerceDB database with the required tables as shown in the image below.

Domain-Driven Design (DDD) in ASP.NET Core Web API

Final Project Structure:

Domain-Driven Design (DDD) in ASP.NET Core Web API with Examples

Benefits of Domain-Driven Design (DDD) in ASP.NET Core Web API
  • Better Alignment with Business: DDD ensures that your software model mirrors the business domain. This reduces the gap between what the business needs and what the software provides.
  • Modularity: DDD’s bounded contexts allow you to break down complex systems into smaller, manageable parts, leading to a more scalable and maintainable architecture.
  • Testability: The separation of concerns in DDD makes it easier to test individual components (e.g., business logic, database access) independently.
  • Flexibility: Since business logic is separated from infrastructure concerns, you can swap technologies (e.g., change from SQL Server to MongoDB) without affecting core business logic.

Domain-Driven Design (DDD) brings clarity and purpose to complex business applications. By aligning technical implementation with business understanding, DDD improves the long-term health of your software systems. Using ASP.NET Core Web API, EF Core, and SQL Server, you can effectively implement Domain-Driven Design (DDD) with real-world benefits like scalability, maintainability, and testability.

In the next article, I will discuss Project Setup for Microservices using ASP.NET Core Web API. In this article, I explain Domain-Driven Design (DDD) in ASP.NET Core Web API. I hope you enjoy this article, Domain-Driven Design (DDD) in ASP.NET Core Web API with Examples.

Leave a Reply

Your email address will not be published. Required fields are marked *