--- title: Entity description: One of the core concepts in Domain-Driven Design - a domain object with a unique identity --- 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 Entities have the following characteristics: 1. **Unique identity**: every entity has a unique `Id` property 2. **Mutability**: an entity's property values can change, but its identity does not 3. **Identity equality**: two entities are equal if and only if their `Id` values are the same 4. **Lifecycle**: entities have an explicit lifecycle, from creation to deletion ## Entity Base Classes MiCake provides the `Entity` base class for defining entities: ```csharp using MiCake.DDD.Domain; // Use a custom Key type public class Customer : Entity { 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: ```csharp // Uses int as the Key type by default public 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 ### The Id Property An entity's `Id` property is its unique identity: ```csharp public abstract class Entity : IEntity where TKey : notnull { public virtual TKey Id { get; init; } = default!; } ``` Characteristics: - Uses the `init` accessor 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 ```csharp // int type (most common) public class Order : Entity { } // Guid type (recommended for distributed systems) public class Customer : Entity { } // long type (for large data volumes) public class LogEntry : Entity { } // string type (business numbers) public class Invoice : Entity { } ``` ### When to Generate the Id ```csharp // Option 1: generated by a factory method (recommended) public class Product : Entity { 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 constructor public class Customer : Entity { public Customer(string name) { Id = Guid.NewGuid(); // Generated in the constructor Name = name; } } // Option 3: database auto-increment public class Order : Entity { // Id is generated by the database auto-increment, no need to set it manually public Order(Customer customer) { Customer = customer; } } ``` ## Entity Equality ### Equality Comparison Rules The MiCake entity base class implements identity-based equality comparison: ```csharp 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 entity bool areEqual = customer1 == customer2; // true // Even if property values differ, the entities are still equal when the Id is the same Console.WriteLine(customer1.Name); // Zhang San Console.WriteLine(customer2.Name); // Li Si Console.WriteLine(customer1 == customer2); // true ``` ## Domain Events Entities can raise domain events to capture important events that occur in the business. ### Raising Domain Events ```csharp public class Order : Entity { public OrderStatus Status { get; private set; } public List 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 ```csharp // Get all pending events of the entity IReadOnlyCollection 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](/en/domain-driven/domain-event/) chapter for details. ## Business Methods on Entities ### Encapsulating Business Logic Entities should contain related business logic rather than being mere data containers: ```csharp public class BankAccount : Entity { 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 1. **Use private setters**: prevent properties from being modified directly from outside 2. **Validate business rules**: validate business constraints inside methods 3. **Throw domain exceptions**: use `DomainException` to report business errors 4. **Raise domain events (optional)**: record important business events 5. **Keep state consistent**: ensure the entity is in a valid state after a method executes ## Entity vs Aggregate Root ### Regular Entities Regular entities have no independent lifecycle and must belong to some aggregate: ```csharp // OrderItem is a regular entity, not an aggregate root public class OrderItem : Entity { 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 An aggregate root is a special entity that can exist independently and serves as the entry point of an aggregate: ```csharp // Order is an aggregate root public class Order : AggregateRoot { private List _items = new(); public IReadOnlyCollection 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` - Aggregate root: `AggregateRoot` (inherits from `Entity`) - Only aggregate roots can have repositories - Regular entities can only be accessed through the aggregate root See the [Aggregate Root](/en/domain-driven/aggregate-root/) chapter for details. ## Best Practices ### 1. Use Factory Methods to Create Entities ```csharp public class Customer : Entity { 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 ```csharp public class Order : Entity { private List _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 ```csharp public class Product : Entity { // 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 ```csharp public class ShoppingCart : Entity { private List _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 ```csharp public class User : Entity { 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 ### ❌ Anemic Model ```csharp // Wrong: the entity only has data, no behavior public class Order : Entity { public int CustomerId { get; set; } public OrderStatus Status { get; set; } public decimal TotalAmount { get; set; } } // Business logic lives in external services public class OrderService { public void SubmitOrder(Order order) { order.Status = OrderStatus.Submitted; // ... } } ``` ### ✅ Rich Model ```csharp // Correct: the entity contains business logic public class Order : Entity { 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 Entities are a fundamental concept in DDD. In MiCake: - Inherit from the `Entity` or `Entity` base 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](/en/domain-driven/value-object/) to understand immutable objects - Read about [Aggregate Roots](/en/domain-driven/aggregate-root/) to understand aggregate design - Check out [Domain Events](/en/domain-driven/domain-event/) to master event-driven development