From ABP Sensei
Guides ABP Framework v10.x DDD patterns: entities, aggregates, repositories, domain services, application services, DTOs, domain events, and unit of work. Use when designing domain layer or application services.
How this skill is triggered — by the user, by Claude, or both
Slash command
/abp-sensei:abp-dddThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A guide to applying DDD in ABP Framework v10.x (10.4/10.5). Entity, Aggregate Root, Repository, Domain Service, Application Service, and DTO design patterns.
A guide to applying DDD in ABP Framework v10.x (10.4/10.5). Entity, Aggregate Root, Repository, Domain Service, Application Service, and DTO design patterns.
ABP applies a four-layer architecture:
┌─────────────────────────────────────────┐
│ Presentation Layer │ ← MVC, Blazor, Angular, React
├─────────────────────────────────────────┤
│ Application Layer │ ← Application Services, DTOs, UOW
├─────────────────────────────────────────┤
│ Domain Layer │ ← Entities, Aggregates, Domain Services, Repositories (interface)
├─────────────────────────────────────────┤
│ Infrastructure Layer │ ← EF Core, MongoDB, 3rd-party libraries
└─────────────────────────────────────────┘
DDD primarily concerns the Domain and Application layers.
public class Book : Entity<Guid>
{
public string Name { get; set; }
public float Price { get; set; }
protected Book() { } // for ORM deserialization
public Book(Guid id) : base(id) { }
}
GUID Key Best Practices:
id parameter in the constructor and pass it to baseGuid.NewGuid() — use IGuidGenerator.Create() (sequential GUID)The Aggregate Root is the single entry point of an aggregate and is responsible for consistency.
public class Order : AggregateRoot<Guid>
{
public virtual string ReferenceNo { get; protected set; }
public virtual int TotalItemCount { get; protected set; }
public virtual DateTime CreationTime { get; protected set; }
public virtual List<OrderLine> OrderLines { get; protected set; }
protected Order() { }
public Order(Guid id, string referenceNo)
{
Check.NotNull(referenceNo, nameof(referenceNo));
Id = id;
ReferenceNo = referenceNo;
OrderLines = new List<OrderLine>();
}
public void AddProduct(Guid productId, int count)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId);
if (existingLine == null)
OrderLines.Add(new OrderLine(Id, productId, count));
else
existingLine.ChangeCount(existingLine.Count + count);
TotalItemCount += count;
}
}
public class OrderLine : Entity
{
public virtual Guid OrderId { get; protected set; }
public virtual Guid ProductId { get; protected set; }
public virtual int Count { get; protected set; }
protected OrderLine() { }
internal OrderLine(Guid orderId, Guid productId, int count)
{
OrderId = orderId;
ProductId = productId;
Count = count;
}
internal void ChangeCount(int newCount) => Count = newCount;
public override object[] GetKeys() => new object[] { OrderId, ProductId };
}
Aggregate Root Rules:
Id, not via a navigation property// Most common usage
public class Product : FullAuditedAggregateRoot<Guid>
{
// Automatic: CreationTime, CreatorId, LastModificationTime,
// LastModifierId, IsDeleted, DeletionTime, DeleterId
public string Name { get; set; }
}
| Base Class | Properties Provided |
|---|---|
CreationAuditedAggregateRoot<TKey> | CreationTime, CreatorId |
AuditedAggregateRoot<TKey> | + LastModificationTime, LastModifierId |
FullAuditedAggregateRoot<TKey> | + IsDeleted, DeletionTime, DeleterId (soft-delete) |
public class Money : ValueObject
{
public decimal Amount { get; private set; }
public string Currency { get; private set; }
private Money() { }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Amount;
yield return Currency;
}
}
public class OrderManager : DomainService
{
private readonly IRepository<Order, Guid> _orderRepository;
public OrderManager(IRepository<Order, Guid> orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<Order> CreateOrderAsync(Guid customerId, List<OrderItem> items)
{
var order = new Order(GuidGenerator.Create(), $"ORD-{DateTime.UtcNow:yyyyMMdd}");
// domain logic
await _orderRepository.InsertAsync(order);
return order;
}
}
Use a domain service when:
Domain Service Best Practices (ai-rules):
*Manager suffix (e.g. OrderManager)IGuidGenerator/IClock — use the base class properties (GuidGenerator, Clock)Aggregate roots publish domain events to trigger side effects. There are two kinds:
public class Order : AggregateRoot<Guid>
{
public OrderStatus Status { get; private set; }
public void Complete()
{
if (Status != OrderStatus.Created)
throw new BusinessException("Orders:CannotCompleteOrder");
Status = OrderStatus.Completed;
// Processed synchronously within the same transaction — can access the entire entity
AddLocalEvent(new OrderCompletedEvent(Id));
// Asynchronous, across modules/microservices — use an ETO (Event Transfer Object)
AddDistributedEvent(new OrderCompletedEto { OrderId = Id });
}
}
Local Event — synchronous, within the same UOW/transaction:
public class OrderCompletedEventHandler
: ILocalEventHandler<OrderCompletedEvent>, ITransientDependency
{
public async Task HandleEventAsync(OrderCompletedEvent eventData)
{
// Runs within the same transaction
}
}
Distributed Event (ETO) — asynchronous, loosely coupled. Define the ETO in *.Domain.Shared:
[EventName("Orders.OrderCompleted")]
public class OrderCompletedEto
{
public Guid OrderId { get; set; }
public string OrderNumber { get; set; }
}
Events are added inside the aggregate root with
AddLocalEvent/AddDistributedEvent; ABP publishes them automatically when the entity is saved. Handlers are registered automatically viaITransientDependency. For distributed event publish/subscribe details see the Infrastructure skill.
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public float Price { get; set; }
}
public class CreateUpdateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
public float Price { get; set; }
}
Rule: Never expose entities to the presentation layer. Always use DTOs.
Since ABP 10.4 the default mapping provider is Mapperly.
[Mapper]
public partial class BookToBookDtoMapper : MapperBase<Book, BookDto>
{
public override partial BookDto Map(Book source);
public override partial void Map(Book source, BookDto destination);
}
[Mapper]
public partial class CreateUpdateBookDtoToBookMapper : MapperBase<CreateUpdateBookDto, Book>
{
public override partial Book Map(CreateUpdateBookDto source);
public override partial void Map(CreateUpdateBookDto source, Book destination);
}
Module configuration:
[DependsOn(typeof(AbpMapperlyModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddMapperlyObjectMapper<MyModule>();
}
}
Usage:
var bookDto = ObjectMapper.Map<Book, BookDto>(book);
var book = ObjectMapper.Map<CreateUpdateBookDto, Book>(input);
public interface IBookAppService : IApplicationService
{
Task<BookDto> GetAsync(Guid id);
Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input);
Task<BookDto> CreateAsync(CreateUpdateBookDto input);
Task<BookDto> UpdateAsync(Guid id, CreateUpdateBookDto input);
Task DeleteAsync(Guid id);
}
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookAppService(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task<BookDto> GetAsync(Guid id)
{
var book = await _bookRepository.GetAsync(id);
return ObjectMapper.Map<Book, BookDto>(book);
}
public async Task<BookDto> CreateAsync(CreateUpdateBookDto input)
{
var book = new Book(GuidGenerator.Create(), input.Name, input.Type, input.Price);
await _bookRepository.InsertAsync(book);
return ObjectMapper.Map<Book, BookDto>(book);
}
}
public interface IBookAppService : ICrudAppService<
BookDto, // TEntityDto
Guid, // TKey
PagedAndSortedResultRequestDto, // TGetListInput
CreateUpdateBookDto, // TCreateInput
CreateUpdateBookDto> // TUpdateInput
{
}
public class BookAppService : CrudAppService<
Book, BookDto, Guid,
PagedAndSortedResultRequestDto,
CreateUpdateBookDto, CreateUpdateBookDto>, IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository) : base(repository) { }
}
ICrudAppService automatically provides: GetAsync, GetListAsync, CreateAsync, UpdateAsync, DeleteAsync
public class DistrictKey
{
public Guid CityId { get; set; }
public string Name { get; set; }
}
public class DistrictAppService : AbstractKeyCrudAppService<District, DistrictDto, DistrictKey>
{
public DistrictAppService(IRepository<District> repository) : base(repository) { }
protected override async Task DeleteByIdAsync(DistrictKey id)
{
await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name);
}
protected override async Task<District> GetEntityByIdAsync(DistrictKey id)
{
var queryable = await Repository.GetQueryableAsync();
return await AsyncQueryableExecuter.FirstOrDefaultAsync(
queryable.Where(d => d.CityId == id.CityId && d.Name == id.Name)
);
}
}
public class BookAppService : CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto, CreateUpdateBookDto>
{
public BookAppService(IRepository<Book, Guid> repository) : base(repository)
{
GetPolicyName = "BookStore.Books";
GetListPolicyName = "BookStore.Books";
CreatePolicyName = "BookStore.Books.Create";
UpdatePolicyName = "BookStore.Books.Update";
DeletePolicyName = "BookStore.Books.Delete";
}
}
public class PersonAppService : ApplicationService
{
private readonly IRepository<Person, Guid> _personRepository;
public PersonAppService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
// Standard methods:
// GetAsync, FindAsync, InsertAsync, UpdateAsync, DeleteAsync
// GetListAsync, GetPagedListAsync, GetCountAsync
// InsertManyAsync, UpdateManyAsync, DeleteManyAsync
public async Task<List<Person>> SearchAsync(string filter)
{
var queryable = await _personRepository.GetQueryableAsync();
return await AsyncExecuter.ToListAsync(
queryable.Where(p => p.Name.Contains(filter))
);
}
}
// Domain layer — interface
public interface IPersonRepository : IRepository<Person, Guid>
{
Task<Person> FindByNameAsync(string name);
}
// EF Core layer — implementation
public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPersonRepository
{
public PersonRepository(IDbContextProvider<MyDbContext> dbContextProvider)
: base(dbContextProvider) { }
public async Task<Person> FindByNameAsync(string name)
{
var dbContext = await GetDbContextAsync();
return await dbContext.Set<Person>()
.FirstOrDefaultAsync(p => p.Name == name);
}
}
var queryable = await _orderRepository.WithDetailsAsync(x => x.OrderLines);
var orders = await AsyncExecuter.ToListAsync(queryable);
// Disable tracking (performance boost for read-only queries)
using (_personRepository.DisableTracking())
{
var list = await _personRepository.GetPagedListAsync(0, 100, "Name ASC");
}
// With an attribute
[DisableEntityChangeTracking]
public virtual async Task<List<PersonDto>> GetListAsync() { ... }
ABP starts a UOW automatically for:
HTTP GET requests do not start a transactional UOW (read-only only).
public class MyService : ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public MyService(IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
public async Task FooAsync()
{
using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
// operations
await uow.CompleteAsync();
}
}
}
[UnitOfWork]
public virtual async Task FooAsync() { }
[UnitOfWork(IsDisabled = true)]
public virtual async Task BarAsync() { }
[UnitOfWork(IsTransactional = false)]
public virtual async Task BazAsync() { }
// autoSave parameter (preferred)
await _repository.InsertAsync(entity, autoSave: true);
// Manual
await UnitOfWorkManager.Current.SaveChangesAsync();
// or
await CurrentUnitOfWork.SaveChangesAsync();
Application service inputs are validated automatically:
public class CreateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
}
Fluent Validation support is also available:
public class CreateBookDtoValidator : AbstractValidator<CreateBookDto>
{
public CreateBookDtoValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
RuleFor(x => x.Price).GreaterThan(0);
}
}
public class ProductsByCategorySpec : Specification<Product>
{
private readonly Guid _categoryId;
public ProductsByCategorySpec(Guid categoryId) => _categoryId = categoryId;
public override Expression<Func<Product, bool>> ToExpression()
{
return p => p.CategoryId == _categoryId && !p.IsDeleted;
}
}
// Usage
var spec = new ProductsByCategorySpec(categoryId);
var products = await _productRepository.GetListAsync(spec);
var count = await _productRepository.CountAsync(spec);
Adding dynamic properties to entities (especially for module extension):
// Set
user.SetProperty("Title", "Dr.");
// Get
var title = user.GetProperty<string>("Title");
// With an extension method
public static class IdentityUserExtensions
{
private const string TitlePropertyName = "Title";
public static void SetTitle(this IdentityUser user, string title) =>
user.SetProperty(TitlePropertyName, title);
public static string GetTitle(this IdentityUser user) =>
user.GetProperty<string>(TitlePropertyName);
}
// Interface
public interface ITestAppService : IApplicationService
{
Task Upload(Guid id, IRemoteStreamContent streamContent);
Task<IRemoteStreamContent> Download(Guid id);
}
// Implementation
public class TestAppService : ApplicationService, ITestAppService
{
public async Task Upload(Guid id, IRemoteStreamContent streamContent)
{
using var fs = new FileStream($"C:\\Temp\\{id}.blob", FileMode.Create);
await streamContent.GetStream().CopyToAsync(fs);
}
public Task<IRemoteStreamContent> Download(Guid id)
{
var fs = new FileStream($"C:\\Temp\\{id}.blob", FileMode.Open);
return Task.FromResult<IRemoteStreamContent>(
new RemoteStreamContent(fs) { ContentType = "application/octet-stream" }
);
}
}
When using a stream in a DTO, the AbpAspNetCoreMvcOptions configuration:
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateFileInput));
});
To enable UOW in classes outside the convention:
public class MyService : ITransientDependency, IUnitOfWorkEnabled
{
// All methods run within a UOW scope
public virtual async Task FooAsync() { } // must be virtual (if not injected via an interface)
}
Rules:
virtual if not injected via an interfaceasync methods (returning Task/Task) are intercepted[Mapper] attributeIGuidGenerator.Create()ISoftDelete implementation is automaticnpx claudepluginhub burakdmir/abp-skills --plugin abp-senseiGuides ABP Framework v10.x development with solution templates, layered architecture, module system, CLI commands, and DDD conventions. Use for creating or working on ABP projects.
Generates .NET domain entities following DDD principles with factory methods, private setters, domain events, and proper encapsulation. Supports aggregate roots, child entities, and value objects.
Implements Domain-Driven Design tactical patterns for .NET: aggregates, value objects, domain events, repositories, and strongly-typed IDs.