Skill

SkillsSoftware Development › Backend & APIs

abp-ddd

ABP DDD patterns – Entities, Aggregate Roots, value objects, Repositories, Domain Services, Domain Events, Specifications. Use when designing domain layer, creating entities, repositories, or domain services in ABP projects.

Freerisk: low
abpddd

The full skill

— name: abp-ddd description: ABP DDD patterns – Entities, Aggregate Roots, value objects, Repositories, Domain Services, Domain Events, Specifications. Use when designing domain layer, creating entities, repositories, or domain services in ABP projects. — # ABP DDD Patterns > **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design ## Anti-Patterns to Avoid – **Anemic entities**: public setters with no behavior — use private setters + methods that enforce invariants – **Repository for child entities**: only aggregate roots get repositories — access child entities through their root – **Generating GUID in entity constructor**: use `IGuidGenerator` from outside and pass `id` parameter – **Navigation properties to other aggregates**: reference by `Id` only, never add full navigation properties across aggregates – **Domain service depending on current user**: accept values from the application layer instead ## Rich Domain Model vs Anemic Domain Model ABP promotes **Rich Domain Model** pattern where entities contain both data AND behavior: | Anemic (Anti-pattern) | Rich (Recommended) | |———————-|——————-| | Entity = data only | Entity = data + behavior | | Logic in services | Logic in entity methods | | Public setters | Private setters with methods | | No validation in entity | Entity enforces invariants | **Encapsulation is key**: Protect entity state by using private setters and exposing behavior through methods. ## Entities ### Entity Example (Rich Model) “`csharp public class OrderLine : Entity<Guid> { public Guid ProductId { get; private set; } public int Count { get; private set; } public decimal Price { get; private set; } protected OrderLine() { } // For ORM internal OrderLine(Guid id, Guid productId, int count, decimal price) : base(id) { ProductId = productId; SetCount(count); // Validates through method Price = price; } public void SetCount(int count) { if (count <= 0) throw new BusinessException("Orders:InvalidCount"); Count = count; } } “` ## Aggregate Roots Aggregate roots are consistency boundaries that: – Own their child entities – Enforce business rules – Publish domain events “`csharp public class Order : AggregateRoot<Guid> { public string OrderNumber { get; private set; } public Guid CustomerId { get; private set; } public OrderStatus Status { get; private set; } public ICollection<OrderLine> Lines { get; private set; } protected Order() { } // For ORM public Order(Guid id, string orderNumber, Guid customerId) : base(id) { OrderNumber = Check.NotNullOrWhiteSpace(orderNumber, nameof(orderNumber)); CustomerId = customerId; Status = OrderStatus.Created; Lines = new List<OrderLine>(); } public void AddLine(Guid lineId, Guid productId, int count, decimal price) { // Business rule: Can only add lines to created orders if (Status != OrderStatus.Created) throw new BusinessException("Orders:CannotModifyOrder"); Lines.Add(new OrderLine(lineId, productId, count, price)); } public void Complete() { if (Status != OrderStatus.Created) throw new BusinessException("Orders:CannotCompleteOrder"); Status = OrderStatus.Completed; // Publish events for side effects AddLocalEvent(new OrderCompletedEvent(Id)); // Same transaction AddDistributedEvent(new OrderCompletedEto { OrderId = Id }); // Cross-service } } “` ### Domain Events – `AddLocalEvent()` – Handled within same transaction, can access full entity – `AddDistributedEvent()` – Handled asynchronously, use ETOs (Event Transfer Objects) ### Entity Best Practices – **Encapsulation**: Private setters, public methods that enforce rules – **Primary constructor**: Enforce invariants, accept `id` parameter – **Protected parameterless constructor**: Required for ORM – **Initialize collections**: In primary constructor – **Virtual members**: For ORM proxy compatibility – **Reference by Id**: Don't add navigation properties to other aggregates – **Don't generate GUID in constructor**: Use `IGuidGenerator` externally ## Repository Pattern ### When to Use Custom Repository – **Generic repository** (`IRepository<T, TKey>`): Sufficient for simple CRUD operations – **Custom repository**: Only when you need custom query methods ### Interface (Domain Layer) “`csharp // Define custom interface only when custom queries are needed public interface IOrderRepository : IRepository<Order, Guid> { Task<Order> FindByOrderNumberAsync(string orderNumber, bool includeDetails = false); Task<List<Order>> GetListByCustomerAsync(Guid customerId, bool includeDetails = false); } “` ### Repository Best Practices – **One repository per aggregate root only** – Never create repositories for child entities – Child entities must be accessed/modified only through their aggregate root – Creating repositories for child entities breaks data consistency (bypasses aggregate root's business rules) – In ABP, use `AddDefaultRepositories()` without `includeAllEntities: true` to enforce this – Define custom repository only when custom queries are needed – ABP handles `CancellationToken` automatically; add parameter only for explicit cancellation control – Single entity methods: `includeDetails = true` by default – List methods: `includeDetails = false` by default – Don't return projection classes – Interface in Domain, implementation in data layer “`csharp // ✅ Correct: Repository for aggregate root (Order) public interface IOrderRepository : IRepository<Order, Guid> { } // ❌ Wrong: Repository for child entity (OrderLine) // OrderLine should only be accessed through Order aggregate public interface IOrderLineRepository : IRepository<OrderLine, Guid> { } // Don't do this! “` ## Domain Services Use domain services for business logic that: – Spans multiple aggregates – Requires repository queries to enforce rules “`csharp public class OrderManager : DomainService { private readonly IOrderRepository _orderRepository; private readonly IProductRepository _productRepository; public OrderManager( IOrderRepository orderRepository, IProductRepository productRepository) { _orderRepository = orderRepository; _productRepository = productRepository; } public async Task<Order> CreateAsync(string orderNumber, Guid customerId) { // Business rule: Order number must be unique var existing = await _orderRepository.FindByOrderNumberAsync(orderNumber); if (existing != null) { throw new BusinessException("Orders:OrderNumberAlreadyExists") .WithData("OrderNumber", orderNumber); } return new Order(GuidGenerator.Create(), orderNumber, customerId); } public async Task AddProductAsync(Order order, Guid productId, int count) { var product = await _productRepository.GetAsync(productId); order.AddLine(productId, count, product.Price); } } “` ### Domain Service Best Practices – Use `*Manager` suffix naming – No interface by default (create only if needed) – Accept/return domain objects, not DTOs – Don't depend on authenticated user – pass values from application layer – Use base class properties (`GuidGenerator`, `Clock`) instead of injecting these services ## Domain Events ### Local Events “`csharp // In aggregate AddLocalEvent(new OrderCompletedEvent(Id)); // Handler public class OrderCompletedEventHandler : ILocalEventHandler<OrderCompletedEvent>, ITransientDependency { public async Task HandleEventAsync(OrderCompletedEvent eventData) { // Handle within same transaction } } “` ### Distributed Events (ETO) For inter-module/microservice communication: “`csharp // In Domain.Shared [EventName("Orders.OrderCompleted")] public class OrderCompletedEto { public Guid OrderId { get; set; } public string OrderNumber { get; set; } } “` ## Specifications Reusable query conditions: “`csharp public class CompletedOrdersSpec : Specification<Order> { public override Expression<Func<Order, bool>> ToExpression() { return o => o.Status == OrderStatus.Completed; } } // Usage var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec()); “`