This ABP Suite issue is also reported as a request in https://abp.io/qa/questions/8803/3a18c029-e901-3c3a-86d9-0bc6255b445f. But I am reporting it as issue because I think it is important.
When creating a master - child entities, ABP suite is generating managers (DomainService) for master (AggregateRoot) and child (Entity) entities. This means child entities can be managed directly from child services (application and domain services) without using the master entity, and I think it is not DDD fridendly. My suggestions for ABP Suite is:
- Do not create the manager (DomainService) for the child entity
- Create methods for managing child entities within the master managers (DomainService). In that way the DomainServices for the AggregateRoot entity will take care about its child entities being more DDD friendly.
1 Answer(s)
-
0
- Solution
You’re right about DDD: only aggregates should expose invariants and lifecycle of their children. In ABP terms, that usually means:
- The AggregateRoot has the public API (entity methods) to create/update/remove its child entities.
- A DomainService (Manager) coordinates rules that don’t naturally fit on a single entity, typically on the aggregate root – not on the child entity itself.
Current ABP Suite behavior:
- ABP Suite can generate a Manager (DomainService) per entity, including child entities in a master–detail model. This may allow direct manipulation of the child outside of the aggregate root, which is not ideal from a strict DDD perspective.
Practical, DDD-friendly approach you can implement today (even with Suite output):
- Remove the child’s Manager and ApplicationService (or don’t use them).
- Keep a single Manager for the AggregateRoot and add operations for child management there.
- Encapsulate child operations within the AggregateRoot entity methods where possible. Use the Manager only when rules cross entity boundaries.
- Restrict repositories: avoid injecting IRepository<Child, TKey> into application services. Load the AggregateRoot and modify children via AggregateRoot methods.
Example Aggregate root with child management methods:
public class Order : AggregateRoot<Guid> { private readonly List<OrderLine> _lines = new(); public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly(); public void AddLine(Guid productId, int quantity, decimal unitPrice) { if (quantity <= 0) throw new BusinessException("QuantityMustBePositive"); _lines.Add(new OrderLine(GuidGenerator.Create(), Id, productId, quantity, unitPrice)); } public void UpdateLine(Guid lineId, int quantity, decimal unitPrice) { var line = _lines.FirstOrDefault(l => l.Id == lineId) ?? throw new BusinessException("LineNotFound"); line.SetQuantity(quantity); line.SetUnitPrice(unitPrice); } public void RemoveLine(Guid lineId) { var line = _lines.FirstOrDefault(l => l.Id == lineId) ?? throw new BusinessException("LineNotFound"); _lines.Remove(line); } } public class OrderManager : DomainService { // Use when orchestration/rules go beyond a single entity, otherwise prefer entity methods public void AddLine(Order order, Guid productId, int quantity, decimal unitPrice) { // cross-entity checks or policies, then delegate to entity order.AddLine(productId, quantity, unitPrice); } }Application service using only the aggregate:
public class OrderAppService : ApplicationService { private readonly IRepository<Order, Guid> _orderRepository; private readonly OrderManager _orderManager; public OrderAppService(IRepository<Order, Guid> orderRepository, OrderManager orderManager) { _orderRepository = orderRepository; _orderManager = orderManager; } public async Task AddLineAsync(Guid orderId, Guid productId, int quantity, decimal unitPrice) { var order = await _orderRepository.GetAsync(orderId); _orderManager.AddLine(order, productId, quantity, unitPrice); await _orderRepository.UpdateAsync(order, autoSave: true); } }What to request for ABP Suite
- Add an option in the master–detail wizard: “Generate child DomainService/ApplicationService: Yes/No”.
- If “No”, generate:
- No Manager for child entity.
- Methods for child CRUD inside the master Manager (and optionally inside the AggregateRoot as entity methods).
- Application service methods that operate on the master aggregate only.
- If “Yes”, keep current behavior for teams that prefer looser aggregate boundaries.
Workaround to align current Suite output with DDD:
- Delete child Manager and AppService classes and their DI registrations.
- Remove child permissions and routes if generated for direct CRUD.
- Move child CRUD endpoints under the master AppService (e.g., OrderAppService.AddLineAsync).
- Ensure repositories for child entities are not injected into external services. Only the master repository should be used.
- Sources
- ABP Suite (tool overview and generation scope):
- https://abp.io/docs/latest/abp-suite
- ABP Domain Services (when and how to use Managers) and DDD layering guidance:
- No verified source available for this specific query. However, this approach aligns with common ABP conventions on aggregates and domain services as widely discussed in ABP’s DDD materials.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.