Создадим фундамент Domain Layer -- абстрактные классы, от которых наследуются все бизнес-объекты.
Структура папок в Domain
Создайте папки в OrderManagement.Domain:
OrderManagement.Domain/
├── Common/ <- базовые классы
├── Entities/ <- Order, OrderItem
├── ValueObjects/ <- Money, Address
├── Enums/ <- OrderStatus
├── Events/ <- события
├── Exceptions/ <- ошибки
└── Interfaces/ <- контракты
IDomainEvent.cs
using MediatR;
namespace OrderManagement.Domain.Common;
public interface IDomainEvent : INotification
{
Guid EventId { get; }
DateTime OccurredOn { get; }
}
Наследование от INotification позволяет публиковать события через _mediator.Publish(). Guid EventId -- уникальный идентификатор конкретного события, DateTime OccurredOn -- время.
Entity.cs
namespace OrderManagement.Domain.Common;
public abstract class Entity
{
public Guid Id { get; protected set; }
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents =>
_domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent) =>
_domainEvents.Add(domainEvent);
public void ClearDomainEvents() => _domainEvents.Clear();
public override bool Equals(object? obj)
{
if (obj is not Entity other) return false;
if (ReferenceEquals(this, other)) return true;
if (GetType() != other.GetType()) return false;
return Id == other.Id;
}
public override int GetHashCode() => Id.GetHashCode();
}
abstract означает: нельзя создать new Entity(). Можно только наследовать: class Order : Entity. protected set для Id -- запись доступна только из класса и его наследников. IReadOnlyCollection запрещает внешнему коду модифицировать список событий.
IAggregateRoot.cs
namespace OrderManagement.Domain.Common;
public interface IAggregateRoot { }
Маркерный интерфейс без методов. Документирует намерение: класс с IAggregateRoot -- корень агрегата.
ValueObject.cs
namespace OrderManagement.Domain.Common;
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj is null || obj.GetType() != GetType()) return false;
return GetEqualityComponents()
.SequenceEqual(((ValueObject)obj).GetEqualityComponents());
}
public override int GetHashCode() =>
GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
public static bool operator ==(ValueObject? a, ValueObject? b)
{
if (a is null && b is null) return true;
if (a is null || b is null) return false;
return a.Equals(b);
}
public static bool operator !=(ValueObject? a, ValueObject? b) =>
!(a == b);
}
GetEqualityComponents() -- наследники возвращают список значимых полей. SequenceEqual сравнивает коллекции поэлементно. Перегрузка == и != позволяет писать money1 == money2.
DomainException.cs
namespace OrderManagement.Domain.Exceptions;
public class DomainException : Exception
{
public DomainException(string message) : base(message) { }
}
public class OrderNotFoundException : DomainException
{
public OrderNotFoundException(Guid orderId)
: base($"Order with ID {orderId} not found") { }
}
DomainException -- ошибка бизнес-правила (HTTP 400). OrderNotFoundException -- заказ не найден (HTTP 404).
Проверка
Ctrl+Shift+B -> Build succeeded