.NET  

🧱 Lesson 2B -Implementing Domain Entities in Clean Architecture (.NET 8)

Previous lesson: 🧱 Lesson 2A— Creating the base solution, API project, folder structure, dependency injection, environment configuration

🚀 Introduction

In any Clean Architecture or Domain-Driven Design (DDD) project, the Domain Layer is the heart of the system.

 It contains the core business entities, rules, and relationships that define what your system does - independent of how it's implemented (no database, UI, or framework dependencies).

In this lecture, we'll implement the core entities of our E-Commerce System:

  • Customer - who makes purchases

  • Product - what is being sold

  • Order - represents a customer's purchase

  • OrderItem - the individual items within an order

By the end, we'll have a pure, framework-agnostic model layer ready to connect with our Application and Infrastructure layers later on.

🧱 Clean Architecture Recap

Before we dive into the code, recall the four layers in our solution:

1

This lecture focuses entirely on ECommerce.Domain.

Domain Folder Structure

Create this structure inside the ECommerce.Domain project:

2

Entities Overview

We'll define four entities:

  • Customer represents a buyer or user placing orders

  • Product represents an item available for sale

  • Order Represents a transaction made by a customer

  • OrderItem represents a line item within an order

1. Customer.cs

Path: ECommerce.Domain/Entities/Customer.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ECommerce.Domain.Entities;

public class Customer
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Required(ErrorMessage = "First name is required.")]
    [MaxLength(50, ErrorMessage = "First name cannot exceed 50 characters.")]
    public string FirstName { get; set; } = string.Empty;

    [Required(ErrorMessage = "Last name is required.")]
    [MaxLength(50, ErrorMessage = "Last name cannot exceed 50 characters.")]
    public string LastName { get; set; } = string.Empty;

    [Required(ErrorMessage = "Email is required.")]
    [EmailAddress(ErrorMessage = "Invalid email address.")]
    [MaxLength(100, ErrorMessage = "Email cannot exceed 100 characters.")]
    public string Email { get; set; } = string.Empty;

    [Phone(ErrorMessage = "Invalid phone number.")]
    [MaxLength(20, ErrorMessage = "Phone number cannot exceed 20 characters.")]
    public string PhoneNumber { get; set; } = string.Empty;

    // Navigation Property
    public ICollection<Order> Orders { get; set; } = new List<Order>();

    // Computed Property (Not mapped to DB)
    [NotMapped]
    public string FullName => $"{FirstName} {LastName}";
}

2. Product.cs
Path: ECommerce.Domain/Entities/Product.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ECommerce.Domain.Entities;

public class Product
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Required(ErrorMessage = "Product name is required.")]
    [MaxLength(100, ErrorMessage = "Product name cannot exceed 100 characters.")]
    public string Name { get; set; } = string.Empty;

    [MaxLength(500, ErrorMessage = "Description cannot exceed 500 characters.")]
    public string Description { get; set; } = string.Empty;

    [Range(0, double.MaxValue, ErrorMessage = "Price must be a non-negative value.")]
    [Column(TypeName = "decimal(18,2)")] // Ensures proper precision in database
    public decimal Price { get; set; }

    [Range(0, int.MaxValue, ErrorMessage = "Stock cannot be negative.")]
    public int Stock { get; set; }

    // Domain Logic
    public void ReduceStock(int quantity)
    {
        if (quantity > Stock)
            throw new InvalidOperationException("Insufficient stock.");
        Stock -= quantity;
    }
}

3. OrderItem.cs
Path: ECommerce.Domain/Entities/OrderItem.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ECommerce.Domain.Entities;

public class OrderItem
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Required]
    public Guid OrderId { get; set; }

    [Required]
    public Guid ProductId { get; set; }

    [Required(ErrorMessage = "Product name is required.")]
    [MaxLength(100, ErrorMessage = "Product name cannot exceed 100 characters.")]
    public string ProductName { get; set; } = string.Empty;

    [Range(0, double.MaxValue, ErrorMessage = "Unit price must be non-negative.")]
    [Column(TypeName = "decimal(18,2)")]
    public decimal UnitPrice { get; set; }

    [Range(1, int.MaxValue, ErrorMessage = "Quantity must be at least 1.")]
    public int Quantity { get; set; }

    // Navigation Property
    public Order? Order { get; set; }

    // Computed Property (Not mapped to DB)
    [NotMapped]
    public decimal TotalPrice => UnitPrice * Quantity;
}

4. Order Status Enum
Path: ECommerce.Domain/Enums/OrderStatus.cs

namespace ECommerce.Domain.Enums
{
    public enum OrderStatus
    {
        Pending,
        Completed,
        Cancelled
    }
}

4. Order.cs
Path: ECommerce.Domain/Entities/Order.cs

using ECommerce.Domain.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ECommerce.Domain.Entities
{
    public class Order
    {
        [Key]
        public Guid Id { get; set; } = Guid.NewGuid();

        [Required]
        public Guid CustomerId { get; set; }

        [Required]
        public DateTime OrderDate { get; set; } = DateTime.UtcNow;

        [Range(0, double.MaxValue, ErrorMessage = "Total amount cannot be negative.")]
        [Column(TypeName = "decimal(18,2)")]
        public decimal TotalAmount { get; private set; }

        [Required]
        public OrderStatus Status { get; set; } = OrderStatus.Pending;

        // Navigation Properties
        public Customer? Customer { get; set; }
        public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();

        // Business Logic
        public void CalculateTotal()
        {
            TotalAmount = Items.Sum(i => i.TotalPrice);
        }

        public void MarkAsCompleted() => Status = OrderStatus.Completed;
        public void MarkAsCancelled() => Status = OrderStatus.Cancelled;
    }
}
3

Relationships Summary

EntityRelationshipDescription
Customer→ Order1-to-Many: A customer can have multiple orders
Order→ CustomerMany-to-1: Each order belongs to one customer
Order→ OrderItem1-to-Many: Each order can contain many items
OrderItem→ OrderMany-to-1: Each item belongs to one order
OrderItem→ ProductReference: Product data copied into order item

Next Lecture Preview

Lecture 2C: Implementing Repository Interfaces (Domain Layer)
We'll define clean contracts for data access:

  • IProductRepository

  • ICustomerRepository

  • IOrderRepository

  • IOrderItemRepository

These interfaces will later be implemented in the Infrastructure layer using EF Core.