DEV Community

Cover image for 🧱 Lesson 2B -Implementing Domain Entities in Clean Architecture (.NET 8)
Farrukh Rehman
Farrukh Rehman

Posted on

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

Series: From Code to Cloud: Building a Production-Ready .NET Application
By: Farrukh Rehman - Senior .NET Full Stack Developer / Team Lead
LinkedIn: https://linkedin.com/in/farrukh-rehman
GitHub: https://github.com/farrukh1212cs

Source Code Backend : https://github.com/farrukh1212cs/ECommerce-Backend.git

Source Code Frontend : https://github.com/farrukh1212cs/ECommerce-Frontend.git

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:

This lecture focuses entirely on ECommerce.Domain.

Domain Folder Structure
Create this structure inside the ECommerce.Domain project:

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}";
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

namespace ECommerce.Domain.Enums
{
    public enum OrderStatus
    {
        Pending,
        Completed,
        Cancelled
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Relationships Summary

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.

Top comments (0)