--- 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 ```csharp public class Patient : FullAuditedAggregateRoot { 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 ```csharp // 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) ```csharp [Authorize({ProjectName}Permissions.Patients.Default)] public class PatientAppService : ApplicationService, IPatientAppService { private readonly IRepository _patientRepository; public PatientAppService(IRepository patientRepository) { _patientRepository = patientRepository; } public async Task> 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(totalCount, patients.ToDto()); } public async Task GetAsync(Guid id) { var patient = await _patientRepository.GetAsync(id); return patient.ToDto(); } [Authorize({ProjectName}Permissions.Patients.Create)] public async Task 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 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) ```csharp // 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 repository) : base(repository) { } // Override specific methods if needed protected override async Task> 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) ```csharp // 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 ToDto(this List patients); // IEnumerable for LINQ public static partial IEnumerable ToDto(this IEnumerable 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) ```csharp // 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 MapList(List patients); } // Register in Module: context.Services.AddSingleton(); ``` ## Permission Patterns ### Permission Constants ```csharp // 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 ```csharp 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 ```csharp 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 ```csharp 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 1. **Entity encapsulation** - Use private setters and domain methods 2. **Thin AppServices** - Orchestrate, don't implement business logic 3. **Domain Services** - For cross-entity business rules 4. **Static Mappers** - Use extension methods for cleaner code 5. **WhereIf pattern** - Clean optional filtering 6. **Permission checks** - Class-level `[Authorize]` + method-level for mutations 7. **autoSave: true** - Use for single operations (avoid extra SaveChanges) 8. **Constants classes** - Define max lengths in Domain.Shared ### Don'ts 1. **Don't duplicate authorization** - If class has `[Authorize]`, methods with same permission don't need it 2. **Don't inject mappers** - Use static extension methods instead 3. **Don't check null after WhereIf** - The `.HasValue` check handles it 4. **Don't use AutoMapper** - Use Mapperly for source generation 5. **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: true` for single operations ## Detailed Reference For comprehensive patterns, see the focused skills: - **`abp-entity-patterns`** - Entity base classes, repositories, domain services, data seeding - **`abp-service-patterns`** - AppServices, DTOs, Mapperly, UoW, Filter DTOs, CommonDependencies - **`abp-infrastructure-patterns`** - Permissions, background jobs, distributed events, multi-tenancy - **`abp-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/