| name | abp-ddd |
| description | ABP Framework v10.x (10.4/10.5) DDD quick reference: Entity, AggregateRoot, repository, domain service, application service, DTO, domain events, specification, UOW. Use when designing the domain layer, entities, aggregates, or repositories in ABP. |
ABP Framework — Domain Driven Design (DDD)
ABP Framework v10.x (10.4/10.5) DDD quick reference. Entity, Aggregate Root, Repository, Domain/Application Service, DTO.
Trigger
"ABP entity/aggregate root", "ABP repository", "ABP application service", "ABP DTO", "ABP domain service", "ABP unit of work", "ABP DDD".
Layers
Presentation → Application (Services, DTO, UOW) → Domain (Entity, Aggregate, Domain Service, Repo interface) → Infrastructure (EF Core/MongoDB). DDD primarily concerns Domain + Application.
Entity & Aggregate Root
public class Order : AggregateRoot<Guid>
{
public string ReferenceNo { get; private set; }
public ICollection<OrderLine> Lines { get; private set; }
protected Order() { }
public Order(Guid id, string referenceNo) : base(id)
{
ReferenceNo = Check.NotNullOrWhiteSpace(referenceNo, nameof(referenceNo));
Lines = new List<OrderLine>();
}
public void AddProduct(Guid productId, int count)
{
if (count <= 0) throw new BusinessException("Orders:InvalidCount");
Lines.Add(new OrderLine(Id, productId, count));
}
}
Rules: rich model (private setter + method), GuidGenerator.Create() not Guid.NewGuid(), reference by Id (no cross-aggregate navigation), one repository per aggregate root (don't open a repo for a child entity).
Audit base classes
| Base | Provides |
|---|
CreationAuditedAggregateRoot<TKey> | CreationTime, CreatorId |
AuditedAggregateRoot<TKey> | + LastModification* |
FullAuditedAggregateRoot<TKey> | + IsDeleted/Deletion* (soft-delete) |
Value Object
public class Money : ValueObject
{
public decimal Amount { get; private set; }
public string Currency { get; private set; }
protected override IEnumerable<object> GetAtomicValues() { yield return Amount; yield return Currency; }
}
Domain Service
public class OrderManager : DomainService
{
private readonly IOrderRepository _orderRepository;
public OrderManager(IOrderRepository orderRepository) => _orderRepository = orderRepository;
public async Task<Order> CreateAsync(string referenceNo)
{
if (await _orderRepository.FindByReferenceAsync(referenceNo) != null)
throw new BusinessException("Orders:ReferenceAlreadyExists");
return new Order(GuidGenerator.Create(), referenceNo);
}
}
Use: when a rule doesn't fit a single entity / when multiple aggregates are needed. Take/return domain objects, not DTOs; don't depend on the authenticated user.
Domain Events
public void Complete()
{
Status = OrderStatus.Completed;
AddLocalEvent(new OrderCompletedEvent(Id));
AddDistributedEvent(new OrderCompletedEto { OrderId = Id });
}
public class OrderCompletedHandler : ILocalEventHandler<OrderCompletedEvent>, ITransientDependency
{
public async Task HandleEventAsync(OrderCompletedEvent e) { }
}
[EventName("Orders.OrderCompleted")]
public class OrderCompletedEto { public Guid OrderId { get; set; } }
Application Layer
public class BookDto : AuditedEntityDto<Guid> { public string Name { get; set; } public float Price { get; set; } }
public class CreateUpdateBookDto
{
[Required, StringLength(128)] public string Name { get; set; }
[Required] public float Price { get; set; }
}
Rule: never expose entities, always DTOs.
Object Mapping (Mapperly — default since v10.4)
[Mapper]
public partial class BookMapper : MapperBase<Book, BookDto>
{
public override partial BookDto Map(Book source);
public override partial void Map(Book source, BookDto destination);
}
var dto = ObjectMapper.Map<Book, BookDto>(book);
For Mapperly/AutoMapper details see the Object Mapping skill.
Application Service
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book, Guid> _repo;
public BookAppService(IRepository<Book, Guid> repo) => _repo = repo;
[Authorize(BookStorePermissions.Books.Create)]
public async Task<BookDto> CreateAsync(CreateUpdateBookDto input)
{
var book = new Book(GuidGenerator.Create(), input.Name, input.Price);
await _repo.InsertAsync(book);
return ObjectMapper.Map<Book, BookDto>(book);
}
}
CrudAppService (reduces boilerplate)
public class BookAppService
: CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto, CreateUpdateBookDto>, IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository) : base(repository)
{
CreatePolicyName = "BookStore.Books.Create";
}
}
Repository
public interface IBookRepository : IRepository<Book, Guid> { Task<Book> FindByNameAsync(string name); }
public class BookRepository : EfCoreRepository<MyDbContext, Book, Guid>, IBookRepository { }
var q = await _orderRepository.WithDetailsAsync(x => x.Lines);
var list = await AsyncExecuter.ToListAsync(q);
Don't expose IQueryable; don't return a projection class. Details: EF Core.
Unit of Work
UOW is automatic in app service / controller / repository methods. HTTP GET is not transactional.
await _repo.InsertAsync(entity, autoSave: true);
[UnitOfWork(IsTransactional = false)] public virtual async Task FooAsync() { }
using (var uow = _uowManager.Begin(requiresNew: true)) { await uow.CompleteAsync(); }
Specification
public class ProductsByCategorySpec : Specification<Product>
{
private readonly Guid _categoryId;
public ProductsByCategorySpec(Guid id) => _categoryId = id;
public override Expression<Func<Product, bool>> ToExpression() => p => p.CategoryId == _categoryId && !p.IsDeleted;
}
var products = await _productRepository.GetListAsync(new ProductsByCategorySpec(categoryId));
Extra Properties
user.SetProperty("Title", "Dr.");
var title = user.GetProperty<string>("Title");
Best Practices
- Aggregate Root + protected setter + constructor validation
- Use DTOs, don't expose entities
- Mapperly
[Mapper] partial class
IGuidGenerator.Create() sequential GUID, Clock for time
- Soft-delete →
FullAuditedAggregateRoot
- Reduce boilerplate with CrudAppService, rely on UOW conventions
Related