Шаг 6: Сущность Order
Order -- Aggregate Root, самый важный класс проекта. Все изменения внутри агрегата проходят только через его методы.
OrderItem.cs -- позиция заказа
В папке Entities:
using OrderManagement.Domain.Common;
using OrderManagement.Domain.Exceptions;
using OrderManagement.Domain.ValueObjects;
namespace OrderManagement.Domain.Entities;
public class OrderItem : Entity
{
public string ProductId { get; private set; }
public string ProductName { get; private set; }
public Money UnitPrice { get; private set; }
public int Quantity { get; private set; }
public Money TotalPrice => UnitPrice.Multiply(Quantity);
private OrderItem() { }
internal OrderItem(string productId, string productName,
Money unitPrice, int quantity)
{
Id = Guid.NewGuid();
ProductId = productId
?? throw new DomainException("ProductId is required");
ProductName = productName
?? throw new DomainException("ProductName is required");
UnitPrice = unitPrice
?? throw new DomainException("UnitPrice is required");
if (quantity <= 0)
throw new DomainException("Quantity must be positive");
if (quantity > 1000)
throw new DomainException("Max quantity is 1000");
Quantity = quantity;
}
internal void IncreaseQuantity(int additionalQuantity)
{
if (additionalQuantity <= 0)
throw new DomainException("Quantity must be positive");
Quantity += additionalQuantity;
}
}
Конструктор internal -- создать OrderItem можно только из проекта Domain. TotalPrice -- вычисляемое свойство, не хранится в БД.
Order.cs -- Aggregate Root
using OrderManagement.Domain.Common;
using OrderManagement.Domain.Enums;
using OrderManagement.Domain.Events;
using OrderManagement.Domain.Exceptions;
using OrderManagement.Domain.ValueObjects;
namespace OrderManagement.Domain.Entities;
public class Order : Entity, IAggregateRoot
{
private readonly List<OrderItem> _items = new();
public string CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public ShippingAddress ShippingAddress { get; private set; }
public Money TotalAmount { get; private set; }
public DateTime CreatedAt { get; private set; }
public IReadOnlyCollection<OrderItem> Items =>
_items.AsReadOnly();
private Order() { }
public static Order Create(
string customerId, ShippingAddress address)
{
if (string.IsNullOrWhiteSpace(customerId))
throw new DomainException("Customer ID is required");
if (address is null)
throw new DomainException("Address is required");
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId,
ShippingAddress = address,
Status = OrderStatus.Draft,
TotalAmount = Money.Zero(),
CreatedAt = DateTime.UtcNow
};
order.AddDomainEvent(
new OrderCreatedEvent(order.Id, customerId));
return order;
}
public void AddItem(string productId, string productName,
Money unitPrice, int quantity)
{
if (Status != OrderStatus.Draft)
throw new DomainException(
"Can only add items to draft orders");
if (quantity <= 0)
throw new DomainException("Quantity must be positive");
var existing = _items.FirstOrDefault(
i => i.ProductId == productId);
if (existing != null)
existing.IncreaseQuantity(quantity);
else
_items.Add(new OrderItem(
productId, productName, unitPrice, quantity));
RecalculateTotal();
}
public void Confirm()
{
if (Status != OrderStatus.Draft)
throw new DomainException(
"Only draft orders can be confirmed");
if (!_items.Any())
throw new DomainException(
"Cannot confirm an empty order");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id, TotalAmount));
}
public void Cancel(string reason)
{
if (Status is OrderStatus.Shipped or OrderStatus.Delivered)
throw new DomainException(
"Cannot cancel shipped or delivered orders");
if (string.IsNullOrWhiteSpace(reason))
throw new DomainException("Reason is required");
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledEvent(Id, reason));
}
private void RecalculateTotal()
{
TotalAmount = _items
.Select(i => i.TotalPrice)
.Aggregate(Money.Zero(), (acc, m) => acc.Add(m));
}
}
Фабричный метод Create вместо публичного конструктора: при создании генерируется событие. private set для всех свойств -- состояние меняется только через бизнес-методы.