Entity
An Entity is one of the core concepts in Domain-Driven Design. In MiCake, an entity is a domain object with a unique identity, identified and compared by its Id.
What is an Entity
Section titled “What is an Entity”Entities have the following characteristics:
- Unique identity: every entity has a unique
Idproperty - Mutability: an entity’s property values can change, but its identity does not
- Identity equality: two entities are equal if and only if their
Idvalues are the same - Lifecycle: entities have an explicit lifecycle, from creation to deletion
Entity Base Classes
Section titled “Entity Base Classes”MiCake provides the Entity<TKey> base class for defining entities:
using MiCake.DDD.Domain;
// Use a custom Key typepublic class Customer : Entity<Guid>{ public string Name { get; private set; } public string Email { get; private set; } public DateTime RegisterDate { get; private set; }
// Constructor private Customer() { } // Required by EF Core
// MiCake recommends using a static Create method to create domain objects public static Customer Create(Guid id, string name, string email) { return new Customer { Id = id, Name = name, Email = email, RegisterDate = DateTime.UtcNow }; }}For entities using int as the primary key, you can use the simplified version:
// Uses int as the Key type by defaultpublic class Product : Entity{ public string Name { get; private set; } public decimal Price { get; private set; } public int Stock { get; private set; }
private Product() { }
public Product(string name, decimal price, int stock) { Name = name; Price = price; Stock = stock; }}Entity Identity
Section titled “Entity Identity”The Id Property
Section titled “The Id Property”An entity’s Id property is its unique identity:
public abstract class Entity<TKey> : IEntity<TKey> where TKey : notnull{ public virtual TKey Id { get; init; } = default!;}Characteristics:
- Uses the
initaccessor to ensure the Id is immutable after initialization - Can be set in the constructor or in an object initializer
- Supports any non-null type as the Key
Common Key Types
Section titled “Common Key Types”// int type (most common)public class Order : Entity<int> { }
// Guid type (recommended for distributed systems)public class Customer : Entity<Guid> { }
// long type (for large data volumes)public class LogEntry : Entity<long> { }
// string type (business numbers)public class Invoice : Entity<string> { }When to Generate the Id
Section titled “When to Generate the Id”// Option 1: generated by a factory method (recommended)public class Product : Entity<Guid>{ private Product() { }
public static Product Create(string name, decimal price) { return new Product { Id = Guid.NewGuid(), Name = name, Price = price }; }}
// Option 2: generated in the constructorpublic class Customer : Entity<Guid>{ public Customer(string name) { Id = Guid.NewGuid(); // Generated in the constructor Name = name; }}
// Option 3: database auto-incrementpublic class Order : Entity<int>{ // Id is generated by the database auto-increment, no need to set it manually public Order(Customer customer) { Customer = customer; }}Entity Equality
Section titled “Entity Equality”Equality Comparison Rules
Section titled “Equality Comparison Rules”The MiCake entity base class implements identity-based equality comparison:
var customer1 = new Customer(Guid.NewGuid(), "Zhang San", "zhang@example.com");var customer2 = new Customer(customer1.Id, "Li Si", "li@example.com");
// As long as the Id is the same, they are considered the same entitybool areEqual = customer1 == customer2; // true
// Even if property values differ, the entities are still equal when the Id is the sameConsole.WriteLine(customer1.Name); // Zhang SanConsole.WriteLine(customer2.Name); // Li SiConsole.WriteLine(customer1 == customer2); // trueDomain Events
Section titled “Domain Events”Entities can raise domain events to capture important events that occur in the business.
Raising Domain Events
Section titled “Raising Domain Events”public class Order : Entity<int>{ public OrderStatus Status { get; private set; } public List<OrderItem> Items { get; private set; } = new();
public void AddItem(Product product, int quantity) { var item = new OrderItem(product, quantity); Items.Add(item);
// Raise a domain event RaiseDomainEvent(new OrderItemAddedEvent(Id, product.Id, quantity)); }
public void Submit() { if (Status != OrderStatus.Draft) throw new DomainException("Only draft orders can be submitted");
Status = OrderStatus.Submitted;
// Raise the submit event RaiseDomainEvent(new OrderSubmittedEvent(Id, Items.Sum(i => i.TotalPrice))); }}Accessing Domain Events
Section titled “Accessing Domain Events”// Get all pending events of the entityIReadOnlyCollection<IDomainEvent> events = order.DomainEvents;
// Clear all events (usually called automatically by the framework)order.ClearDomainEvents();Events are dispatched automatically when the unit of work commits (IUnitOfWork.CommitAsync()); see the Domain Events chapter for details.
Business Methods on Entities
Section titled “Business Methods on Entities”Encapsulating Business Logic
Section titled “Encapsulating Business Logic”Entities should contain related business logic rather than being mere data containers:
public class BankAccount : Entity<Guid>{ public decimal Balance { get; private set; } public AccountStatus Status { get; private set; }
private BankAccount() { }
public static BankAccount Open(Guid id, decimal initialDeposit) { if (initialDeposit < 0) throw new DomainException("Initial deposit cannot be negative");
var account = new BankAccount { Id = id, Balance = initialDeposit, Status = AccountStatus.Active };
account.RaiseDomainEvent(new AccountOpenedEvent(id, initialDeposit)); return account; }
public void Deposit(decimal amount) { if (amount <= 0) throw new DomainException("Deposit amount must be positive");
if (Status != AccountStatus.Active) throw new DomainException("Account is not active");
Balance += amount; RaiseDomainEvent(new MoneyDepositedEvent(Id, amount, Balance)); }
public void Withdraw(decimal amount) { if (amount <= 0) throw new DomainException("Withdrawal amount must be positive");
if (Status != AccountStatus.Active) throw new DomainException("Account is not active");
if (Balance < amount) throw new DomainException("Insufficient balance");
Balance -= amount; RaiseDomainEvent(new MoneyWithdrawnEvent(Id, amount, Balance)); }
public void Close() { if (Balance != 0) throw new DomainException("Cannot close account with non-zero balance");
Status = AccountStatus.Closed; RaiseDomainEvent(new AccountClosedEvent(Id)); }}Method Design Principles
Section titled “Method Design Principles”- Use private setters: prevent properties from being modified directly from outside
- Validate business rules: validate business constraints inside methods
- Throw domain exceptions: use
DomainExceptionto report business errors - Raise domain events (optional): record important business events
- Keep state consistent: ensure the entity is in a valid state after a method executes
Entity vs Aggregate Root
Section titled “Entity vs Aggregate Root”Regular Entities
Section titled “Regular Entities”Regular entities have no independent lifecycle and must belong to some aggregate:
// OrderItem is a regular entity, not an aggregate rootpublic class OrderItem : Entity<int>{ public int OrderId { get; private set; } public int ProductId { get; private set; } public int Quantity { get; private set; } public decimal Price { get; private set; }
// Can only be accessed and modified through the Order aggregate root}Aggregate Roots
Section titled “Aggregate Roots”An aggregate root is a special entity that can exist independently and serves as the entry point of an aggregate:
// Order is an aggregate rootpublic class Order : AggregateRoot<int>{ private List<OrderItem> _items = new(); public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void AddItem(Product product, int quantity, decimal price) { var item = new OrderItem { ProductId = product.Id, Quantity = quantity, Price = price }; _items.Add(item); }
// All operations on OrderItem go through Order}Key differences:
- Regular entity:
Entity<TKey> - Aggregate root:
AggregateRoot<TKey>(inherits fromEntity<TKey>) - Only aggregate roots can have repositories
- Regular entities can only be accessed through the aggregate root
See the Aggregate Root chapter for details.
Best Practices
Section titled “Best Practices”1. Use Factory Methods to Create Entities
Section titled “1. Use Factory Methods to Create Entities”public class Customer : Entity<Guid>{ private Customer() { }
// Factory methods ensure business rules are satisfied when creating an entity public static Customer Create(string name, string email) { if (string.IsNullOrWhiteSpace(name)) throw new DomainException("Customer name is required");
if (!email.Contains("@")) throw new DomainException("Invalid email format");
return new Customer { Id = Guid.NewGuid(), Name = name, Email = email, RegisterDate = DateTime.UtcNow }; }}2. Keep State Consistent
Section titled “2. Keep State Consistent”public class Order : Entity<int>{ private List<OrderItem> _items = new(); private decimal _totalAmount;
public void AddItem(OrderItem item) { _items.Add(item); // Update the total amount immediately to keep consistency _totalAmount += item.TotalPrice; }
public void RemoveItem(OrderItem item) { if (_items.Remove(item)) { _totalAmount -= item.TotalPrice; } }}3. Use Private Setters to Protect Data
Section titled “3. Use Private Setters to Protect Data”public class Product : Entity<int>{ // Use private set to prevent external modification public string Name { get; private set; } public decimal Price { get; private set; }
// Modify through methods so validation logic can be added public void UpdatePrice(decimal newPrice) { if (newPrice < 0) throw new DomainException("Price cannot be negative");
Price = newPrice; }}4. Validate Business Rules
Section titled “4. Validate Business Rules”public class ShoppingCart : Entity<Guid>{ private List<CartItem> _items = new(); private const int MaxItemsCount = 100;
public void AddItem(Product product, int quantity) { // Business rule validation if (quantity <= 0) throw new DomainException("Quantity must be positive");
if (_items.Count >= MaxItemsCount) throw new DomainException("Cart is full");
if (product.Stock < quantity) throw new DomainException("Insufficient stock");
// Perform the operation _items.Add(new CartItem(product, quantity)); }}5. Use Domain Events Wisely
Section titled “5. Use Domain Events Wisely”public class User : Entity<int>{ public string Email { get; private set; } public bool IsEmailVerified { get; private set; }
public void VerifyEmail(string verificationCode) { // Verification logic if (IsEmailVerified) throw new DomainException("Email already verified");
// Verify the code...
IsEmailVerified = true;
// Raise an event so other parts can respond (e.g., send a welcome email) RaiseDomainEvent(new EmailVerifiedEvent(Id, Email)); }}Common Mistakes
Section titled “Common Mistakes”❌ Anemic Model
Section titled “❌ Anemic Model”// Wrong: the entity only has data, no behaviorpublic class Order : Entity<int>{ public int CustomerId { get; set; } public OrderStatus Status { get; set; } public decimal TotalAmount { get; set; }}
// Business logic lives in external servicespublic class OrderService{ public void SubmitOrder(Order order) { order.Status = OrderStatus.Submitted; // ... }}✅ Rich Model
Section titled “✅ Rich Model”// Correct: the entity contains business logicpublic class Order : Entity<int>{ public int CustomerId { get; private set; } public OrderStatus Status { get; private set; } public decimal TotalAmount { get; private set; }
public void Submit() { if (Status != OrderStatus.Draft) throw new DomainException("Only draft orders can be submitted");
Status = OrderStatus.Submitted; RaiseDomainEvent(new OrderSubmittedEvent(Id)); }}Summary
Section titled “Summary”Entities are a fundamental concept in DDD. In MiCake:
- Inherit from the
Entity<TKey>orEntitybase class - Identified by a unique
Id - Can raise domain events
- Should contain related business logic
- Use private setters to protect data
- Modify state through methods and validate business rules
Next steps:
- Learn about Value Objects to understand immutable objects
- Read about Aggregate Roots to understand aggregate design
- Check out Domain Events to master event-driven development
