| name | abp-framework-patterns |
| description | Master ABP Framework patterns including repository pattern, unit of work, domain services, application services, authorization, multi-tenancy, background jobs, and distributed events. Use when: (1) building ABP-based applications with DDD architecture, (2) creating CRUD services with Entity, AppService, DTOs, validators, (3) handling authorization/permissions, (4) generating ABP module code. |
| layer | 2 |
| tech_stack | dotnet, csharp, abp, efcore |
| topics | entity, appservice, dto, repository, mapping, permissions, domain-service, background-jobs |
| depends_on | csharp-advanced-patterns, dotnet-async-patterns |
| complements | efcore-patterns, fluentvalidation-patterns, openiddict-authorization |
| keywords | Entity, AppService, DTO, Mapperly, Repository, UnitOfWork, IRepository, ApplicationService, CrudAppService |
ABP Framework Patterns
Master ABP Framework patterns for building maintainable, scalable applications following Domain-Driven Design principles.
This skill orchestrates three focused skills:
| Skill | Focus | Key Patterns |
|---|---|---|
abp-entity-patterns |
Domain layer | Entity, Repository, DomainService, DataSeeding |
abp-service-patterns |
Application layer | AppService, DTOs, Mapperly, UoW, Filter DTOs |
abp-infrastructure-patterns |
Cross-cutting | Permissions, BackgroundJobs, Events, Multi-tenancy |
Quick Reference
Architecture Layers
Domain.Shared → Constants, enums, shared types
Domain → Entities, repositories, domain services, domain events
Application.Contracts → DTOs, application service interfaces
Application → Application services, mapper profiles, validators
EntityFrameworkCore → DbContext, repository implementations
HttpApi → Controllers (auto-generated by ABP)
HttpApi.Host → Startup, configuration
Common Patterns
| Pattern | Location | When to Use |
|---|---|---|
| Entity | Domain | Business data with identity |
| Repository | Domain + EFC | Custom data access queries |
| Domain Service | Domain | Cross-entity business logic |
| AppService | Application | Orchestration, authorization, mapping |
| CrudAppService | Application | Simple CRUD without custom logic |
| Validator | Application | Input DTO validation |
| Permission | Domain.Shared | Access control |
| Background Job | Application | Async/delayed processing |
| Distributed Event | Domain/App | Module decoupling |
Entity Patterns
Standard Entity with Encapsulation
public class Patient : FullAuditedAggregateRoot<Guid>
{
public string FirstName { get; private set; } = string.Empty;
public string LastName { get; private set; } = string.Empty;
public string Email { get; private set; } = string.Empty;
public bool IsActive { get; private set; } = true;
// Required for EF Core
protected Patient() { }
public Patient(Guid id, string firstName, string lastName, string email) : base(id)
{
SetName(firstName, lastName);
SetEmail(email);
}
public void SetName(string firstName, string lastName)
{
FirstName = Check.NotNullOrWhiteSpace(firstName, nameof(firstName), maxLength: PatientConsts.MaxFirstNameLength);
LastName = Check.NotNullOrWhiteSpace(lastName, nameof(lastName), maxLength: PatientConsts.MaxLastNameLength);
}
public void SetEmail(string email)
{
Email = Check.NotNullOrWhiteSpace(email, nameof(email), maxLength: PatientConsts.MaxEmailLength);
}
public void Activate() => IsActive = true;
public void Deactivate() => IsActive = false;
}
Constants Class
// Domain.Shared/{Feature}/{Entity}Consts.cs
namespace {ProjectName}.{Feature};
public static class PatientConsts
{
public const int MaxFirstNameLength = 100;
public const int MaxLastNameLength = 100;
public const int MaxEmailLength = 256;
}
AppService Patterns
Standard AppService (Recommended)
[Authorize({ProjectName}Permissions.Patients.Default)]
public class PatientAppService : ApplicationService, IPatientAppService
{
private readonly IRepository<Patient, Guid> _patientRepository;
public PatientAppService(IRepository<Patient, Guid> patientRepository)
{
_patientRepository = patientRepository;
}
public async Task<PagedResultDto<PatientDto>> GetListAsync(GetPatientsInput input)
{
var queryable = await _patientRepository.GetQueryableAsync();
var query = queryable
.WhereIf(!input.Filter.IsNullOrWhiteSpace(),
x => x.FirstName.Contains(input.Filter!) || x.LastName.Contains(input.Filter!))
.WhereIf(input.IsActive.HasValue, x => x.IsActive == input.IsActive);
var totalCount = await AsyncExecuter.CountAsync(query);
var patients = await AsyncExecuter.ToListAsync(
query
.OrderBy(input.Sorting.IsNullOrWhiteSpace() ? nameof(Patient.LastName) : input.Sorting)
.PageBy(input));
return new PagedResultDto<PatientDto>(totalCount, patients.ToDto());
}
public async Task<PatientDto> GetAsync(Guid id)
{
var patient = await _patientRepository.GetAsync(id);
return patient.ToDto();
}
[Authorize({ProjectName}Permissions.Patients.Create)]
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
var patient = new Patient(
GuidGenerator.Create(),
input.FirstName,
input.LastName,
input.Email);
await _patientRepository.InsertAsync(patient, autoSave: true);
return patient.ToDto();
}
[Authorize({ProjectName}Permissions.Patients.Edit)]
public async Task<PatientDto> UpdateAsync(Guid id, UpdatePatientDto input)
{
var patient = await _patientRepository.GetAsync(id);
patient.SetName(input.FirstName, input.LastName);
patient.SetEmail(input.Email);
await _patientRepository.UpdateAsync(patient, autoSave: true);
return patient.ToDto();
}
[Authorize({ProjectName}Permissions.Patients.Delete)]
public async Task DeleteAsync(Guid id)
{
await _patientRepository.DeleteAsync(id);
}
}
CrudAppService Base (For Simple CRUD)
// Use when no custom business logic is needed
public class PatientAppService : CrudAppService<
Patient, // Entity
PatientDto, // Output DTO
Guid, // Primary key
GetPatientsInput, // GetList input
CreatePatientDto, // Create input
UpdatePatientDto>, // Update input
IPatientAppService
{
public PatientAppService(IRepository<Patient, Guid> repository)
: base(repository)
{
}
// Override specific methods if needed
protected override async Task<IQueryable<Patient>> CreateFilteredQueryAsync(GetPatientsInput input)
{
var query = await base.CreateFilteredQueryAsync(input);
return query
.WhereIf(!input.Filter.IsNullOrWhiteSpace(),
x => x.FirstName.Contains(input.Filter!));
}
}
Mapperly Patterns
Static Extension Methods (RECOMMENDED)
// Application/{Feature}/{Entity}Mappers.cs
using Riok.Mapperly.Abstractions;
namespace {ProjectName}.{Feature};
[Mapper]
public static partial class PatientMappers
{
// Single entity mapping
public static partial PatientDto ToDto(this Patient patient);
// Collection mapping
public static partial List<PatientDto> ToDto(this List<Patient> patients);
// IEnumerable for LINQ
public static partial IEnumerable<PatientDto> ToDto(this IEnumerable<Patient> patients);
// Update entity from DTO (ignores Id)
[MapperIgnoreTarget(nameof(Patient.Id))]
public static partial void UpdateFrom(this Patient patient, UpdatePatientDto dto);
}
// Usage in AppService (no injection needed):
return patient.ToDto();
return patients.ToDto().ToList();
Instance Mapper (Alternative)
// Only if DI is required (e.g., for resolving navigation properties)
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.None)]
public partial class PatientApplicationMappers
{
public partial PatientDto Map(Patient patient);
public partial List<PatientDto> MapList(List<Patient> patients);
}
// Register in Module:
context.Services.AddSingleton<PatientApplicationMappers>();
Permission Patterns
Permission Constants
// Application.Contracts/Permissions/{Entity}Permissions.cs
namespace {ProjectName}.Permissions;
public static class PatientPermissions
{
public const string GroupName = "{ProjectName}.Patients";
public const string Default = GroupName;
public const string Create = GroupName + ".Create";
public const string Edit = GroupName + ".Edit";
public const string Delete = GroupName + ".Delete";
}
Permission Definition Provider
public class {ProjectName}PermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup({ProjectName}Permissions.GroupName);
var patientsPermission = myGroup.AddPermission(
PatientPermissions.Default,
L("Permission:Patients"));
patientsPermission.AddChild(
PatientPermissions.Create,
L("Permission:Patients.Create"));
patientsPermission.AddChild(
PatientPermissions.Edit,
L("Permission:Patients.Edit"));
patientsPermission.AddChild(
PatientPermissions.Delete,
L("Permission:Patients.Delete"));
}
private static LocalizableString L(string name) =>
LocalizableString.Create<{ProjectName}Resource>(name);
}
Filter DTO Pattern
Standard Filter DTO
using Volo.Abp.Application.Dtos;
namespace {ProjectName}.{Feature};
public class GetPatientsInput : PagedAndSortedResultRequestDto
{
// Text search filter
public string? Filter { get; set; }
// Status filter
public bool? IsActive { get; set; }
// Foreign key filter
public Guid? DoctorId { get; set; }
// Date range filters
public DateTime? FromDate { get; set; }
public DateTime? ToDate { get; set; }
}
WhereIf Extension Usage
var query = queryable
.WhereIf(!input.Filter.IsNullOrWhiteSpace(),
x => x.FirstName.Contains(input.Filter!) ||
x.LastName.Contains(input.Filter!) ||
x.Email.Contains(input.Filter!))
.WhereIf(input.IsActive.HasValue, x => x.IsActive == input.IsActive)
.WhereIf(input.DoctorId.HasValue, x => x.DoctorId == input.DoctorId)
.WhereIf(input.FromDate.HasValue, x => x.CreationTime >= input.FromDate)
.WhereIf(input.ToDate.HasValue, x => x.CreationTime <= input.ToDate);
Best Practices
Do's
- Entity encapsulation - Use private setters and domain methods
- Thin AppServices - Orchestrate, don't implement business logic
- Domain Services - For cross-entity business rules
- Static Mappers - Use extension methods for cleaner code
- WhereIf pattern - Clean optional filtering
- Permission checks - Class-level
[Authorize]+ method-level for mutations - autoSave: true - Use for single operations (avoid extra SaveChanges)
- Constants classes - Define max lengths in Domain.Shared
Don'ts
- Don't duplicate authorization - If class has
[Authorize], methods with same permission don't need it - Don't inject mappers - Use static extension methods instead
- Don't check null after WhereIf - The
.HasValuecheck handles it - Don't use AutoMapper - Use Mapperly for source generation
- Don't expose entities - Always return DTOs
Code Quality Checklist
Before completing implementation:
- Entity has private setters and domain methods
- Entity has parameterless protected constructor for EF Core
- AppService class has
[Authorize(Permission.Default)] - All mutations have specific
[Authorize(Permission.Action)] - Mapper uses static extension methods pattern
- Filter DTO extends
PagedAndSortedResultRequestDto - Filter uses WhereIf pattern for optional parameters
- Constants defined in Domain.Shared
- Permissions follow
{Project}.{Resource}.{Action}pattern - InsertAsync uses
autoSave: truefor single operations
Detailed Reference
For comprehensive patterns, see the focused skills:
abp-entity-patterns- Entity base classes, repositories, domain services, data seedingabp-service-patterns- AppServices, DTOs, Mapperly, UoW, Filter DTOs, CommonDependenciesabp-infrastructure-patterns- Permissions, background jobs, distributed events, multi-tenancyabp-contract-scaffolding- Interface and DTO generation for parallel workflows
External Resources
- ABP Documentation: https://docs.abp.io/
- ABP Community: https://community.abp.io/
- ABP GitHub: https://github.com/abpframework/abp
- Mapperly: https://mapperly.riok.app/