abp-service-patterns
ABP Framework application layer patterns including AppServices, DTOs, Mapperly mapping, Unit of Work, and common patterns like Filter DTOs and ResponseModel. Use when: (1) creating AppServices, (2) mapping DTOs with Mapperly, (3) implementing list filtering, (4) wrapping API responses.
When & Why to Use This Skill
This Claude skill provides standardized application layer patterns for the ABP Framework, focusing on AppServices, DTO mapping with Mapperly, and Unit of Work management. It helps .NET developers implement clean architecture, ensure consistent API responses, and follow best practices for high-performance C# development within the ABP ecosystem.
Use Cases
- Case 1: Designing and implementing thin Application Services that orchestrate domain logic while handling authorization and DTO conversions.
- Case 2: Migrating from AutoMapper to Mapperly source generators to improve performance and prevent common copy-paste mapping bugs.
- Case 3: Implementing complex server-side filtering and pagination using the Filter DTO pattern and ABP's WhereIf extension methods.
- Case 4: Managing database transaction boundaries using the Unit of Work pattern for multi-repository operations or batch processing.
- Case 5: Standardizing API output and error handling by implementing the ResponseModel wrapper and structured logging across the application layer.
| name | abp-service-patterns |
|---|---|
| description | "ABP Framework application layer patterns including AppServices, DTOs, Mapperly mapping, Unit of Work, and common patterns like Filter DTOs and ResponseModel. Use when: (1) creating AppServices, (2) mapping DTOs with Mapperly, (3) implementing list filtering, (4) wrapping API responses." |
| layer | 2 |
| tech_stack | [dotnet, csharp, abp] |
| topics | [appservice, dto, mapperly, unit-of-work, filter-dto, response-model] |
| depends_on | [csharp-advanced-patterns, dotnet-async-patterns] |
| complements | [abp-entity-patterns, fluentvalidation-patterns] |
| keywords | [AppService, ApplicationService, DTO, Mapperly, UnitOfWork, PagedResultDto, WhereIf] |
ABP Service Patterns
Application layer patterns for ABP Framework.
Application Service Pattern
public class PatientAppService : ApplicationService, IPatientAppService
{
private readonly IRepository<Patient, Guid> _patientRepository;
private readonly PatientManager _patientManager; // Domain service
private readonly ClinicApplicationMappers _mapper;
public PatientAppService(
IRepository<Patient, Guid> patientRepository,
PatientManager patientManager,
ClinicApplicationMappers mapper)
{
_patientRepository = patientRepository;
_patientManager = patientManager;
_mapper = mapper;
}
[Authorize(ClinicPermissions.Patients.Default)]
public async Task<PatientDto> GetAsync(Guid id)
{
var patient = await _patientRepository.GetAsync(id);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Create)]
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
var patient = await _patientManager.CreateAsync(
input.FirstName, input.LastName, input.Email, input.DateOfBirth);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Edit)]
public async Task<PatientDto> UpdateAsync(Guid id, UpdatePatientDto input)
{
var patient = await _patientRepository.GetAsync(id);
_mapper.UpdatePatientFromDto(input, patient);
await _patientRepository.UpdateAsync(patient);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Delete)]
public async Task DeleteAsync(Guid id)
{
await _patientRepository.DeleteAsync(id);
}
}
Object Mapping with Mapperly
ABP 10.x uses Mapperly (source generator) instead of AutoMapper.
// Application/ClinicApplicationMappers.cs
[Mapper]
public partial class ClinicApplicationMappers
{
// Entity to DTO
public partial PatientDto PatientToDto(Patient patient);
public partial List<PatientDto> PatientsToDtos(List<Patient> patients);
// DTO to Entity (creation)
public partial Patient CreateDtoToPatient(CreatePatientDto dto);
// DTO to Entity (update) - ignores Id
[MapperIgnoreTarget(nameof(Patient.Id))]
public partial void UpdatePatientFromDto(UpdatePatientDto dto, Patient patient);
// Complex mapping with navigation properties
[MapProperty(nameof(Appointment.Patient.FirstName), nameof(AppointmentDto.PatientName))]
[MapProperty(nameof(Appointment.Doctor.FullName), nameof(AppointmentDto.DoctorName))]
public partial AppointmentDto AppointmentToDto(Appointment appointment);
}
Register in Module:
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<ClinicApplicationMappers>();
}
Unit of Work
ABP automatically manages UoW for application service methods.
public class AppointmentAppService : ApplicationService
{
// This method is automatically wrapped in a UoW
// All changes are committed together or rolled back on exception
public async Task<AppointmentDto> CreateAsync(CreateAppointmentDto input)
{
var patient = await _patientRepository.GetAsync(input.PatientId);
patient.LastAppointmentDate = input.AppointmentDate;
var appointment = new Appointment(
GuidGenerator.Create(),
input.PatientId,
input.DoctorId,
input.AppointmentDate);
await _appointmentRepository.InsertAsync(appointment);
// Both changes committed together automatically
return _mapper.AppointmentToDto(appointment);
}
}
Manual UoW Control:
[UnitOfWork(isTransactional: false)] // Disable for read-only
public async Task GenerateLargeReportAsync() { }
public async Task ProcessBatchAsync(List<Guid> ids)
{
foreach (var id in ids)
{
using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
{
await ProcessItemAsync(id);
await uow.CompleteAsync();
}
}
}
Filter DTO Pattern
Separate query filters from pagination for clean, self-documenting APIs.
Filter DTO:
public class PatientFilter
{
public Guid? DoctorId { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public bool? IsActive { get; set; }
public DateTime? CreatedAfter { get; set; }
public DateTime? CreatedBefore { get; set; }
}
AppService with WhereIf:
public async Task<PagedResultDto<PatientDto>> GetListAsync(
PagedAndSortedResultRequestDto input,
PatientFilter filter)
{
// Trim string inputs
filter.Name = filter.Name?.Trim();
filter.Email = filter.Email?.Trim();
// Default sorting
if (input.Sorting.IsNullOrWhiteSpace())
input.Sorting = nameof(PatientDto.FirstName);
var queryable = await _patientRepository.GetQueryableAsync();
var query = queryable
.WhereIf(filter.DoctorId.HasValue, x => x.DoctorId == filter.DoctorId)
.WhereIf(!filter.Name.IsNullOrWhiteSpace(),
x => x.FirstName.Contains(filter.Name) || x.LastName.Contains(filter.Name))
.WhereIf(!filter.Email.IsNullOrWhiteSpace(),
x => x.Email.ToLower().Contains(filter.Email.ToLower()))
.WhereIf(filter.IsActive.HasValue, x => x.IsActive == filter.IsActive)
.WhereIf(filter.CreatedAfter.HasValue, x => x.CreationTime >= filter.CreatedAfter)
.WhereIf(filter.CreatedBefore.HasValue, x => x.CreationTime <= filter.CreatedBefore);
var totalCount = await AsyncExecuter.CountAsync(query);
var patients = await AsyncExecuter.ToListAsync(
query.OrderBy(input.Sorting).PageBy(input.SkipCount, input.MaxResultCount));
return new PagedResultDto<PatientDto>(totalCount, _mapper.PatientsToDtos(patients));
}
ResponseModel Wrapper
public class ResponseModel<T>
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public static ResponseModel<T> Success(T data, string message = null)
=> new() { IsSuccess = true, Data = data, Message = message };
public static ResponseModel<T> Failure(string message)
=> new() { IsSuccess = false, Message = message };
}
// Usage
public async Task<ResponseModel<PatientDto>> GetAsync(Guid id)
{
var patient = await _patientRepository.FirstOrDefaultAsync(x => x.Id == id);
if (patient == null)
return ResponseModel<PatientDto>.Failure("Patient not found");
return ResponseModel<PatientDto>.Success(_mapper.PatientToDto(patient));
}
CommonDependencies Pattern
Reduce constructor bloat by grouping cross-cutting dependencies.
public class CommonDependencies<T>
{
public IDistributedEventBus DistributedEventBus { get; set; }
public IDataFilter DataFilter { get; set; }
public ILogger<T> Logger { get; set; }
public IGuidGenerator GuidGenerator { get; set; }
}
// Register
context.Services.AddTransient(typeof(CommonDependencies<>));
// Usage
public class PatientAppService : ApplicationService
{
private readonly IRepository<Patient, Guid> _patientRepository;
private readonly CommonDependencies<PatientAppService> _common;
public PatientAppService(
IRepository<Patient, Guid> patientRepository,
CommonDependencies<PatientAppService> common)
{
_patientRepository = patientRepository;
_common = common;
}
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
_common.Logger.LogInformation("Creating patient: {Name}", input.FirstName);
var patient = new Patient(_common.GuidGenerator.Create(), /*...*/);
await _patientRepository.InsertAsync(patient);
await _common.DistributedEventBus.PublishAsync(new PatientCreatedEto { Id = patient.Id });
return _mapper.PatientToDto(patient);
}
}
Structured Logging
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
_logger.LogInformation(
"[{Service}] {Method} - Started - Input: {@Input}",
nameof(PatientAppService), nameof(CreateAsync), input);
try
{
var patient = await _patientManager.CreateAsync(/*...*/);
_logger.LogInformation(
"[{Service}] {Method} - Completed - PatientId: {PatientId}",
nameof(PatientAppService), nameof(CreateAsync), patient.Id);
return _mapper.PatientToDto(patient);
}
catch (Exception ex)
{
_logger.LogError(ex,
"[{Service}] {Method} - Failed - Error: {Message}",
nameof(PatientAppService), nameof(CreateAsync), ex.Message);
throw;
}
}
Input Sanitization
public static class InputSanitization
{
public static string TrimAndLower(this string value) => value?.Trim()?.ToLowerInvariant();
public static string TrimAndUpper(this string value) => value?.Trim()?.ToUpperInvariant();
}
// Usage
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
input.Email = input.Email.TrimAndLower();
input.FirstName = input.FirstName?.Trim();
// ...
}
Mapping Validation Patterns
Common Bug: Copy-Paste Property Mapping
Manual mappings (especially in select new clauses) are prone to copy-paste errors:
// ❌ BUG: Wrong property copied - IsPutawayCompleted mapped from wrong source!
select new LicensePlateDto()
{
IsInboundQCChecklistCompleted = lc.IsInboundQCChecklistCompleted,
IsPutawayCompleted = lc.IsInboundQCChecklistCompleted, // BUG! Should be lc.IsPutawayCompleted
IsHold = lc.IsHold
}
// ✅ CORRECT: Use Mapperly to prevent copy-paste errors
[Mapper]
public partial class LicensePlateMapper
{
public partial LicensePlateDto ToDto(LicensePlate entity);
}
// Or if manual mapping is required, double-check similar-named properties
select new LicensePlateDto()
{
IsInboundQCChecklistCompleted = lc.IsInboundQCChecklistCompleted,
IsPutawayCompleted = lc.IsPutawayCompleted, // ✅ Correct property
IsHold = lc.IsHold
}
Manual Mapping Checklist
When manual mapping is unavoidable (e.g., complex projections), verify:
- Each DTO property maps to the correct entity property
- Similar-named properties double-checked (e.g.,
IsXxxCompletedvsIsYyyCompleted) - Null checks on optional navigation properties
- No copy-paste from adjacent lines without modification
High-Risk Property Patterns
Be extra careful with these patterns that look similar:
| DTO Property | Wrong Source | Correct Source |
|---|---|---|
IsPutawayCompleted |
entity.IsInboundCompleted |
entity.IsPutawayCompleted |
UpdatedAt |
entity.CreatedAt |
entity.LastModificationTime |
CustomerName |
entity.ShipperName |
entity.CustomerName |
TargetDate |
entity.SourceDate |
entity.TargetDate |
Best Practices
- Thin AppServices - Orchestrate, don't implement business logic
- Delegate to Domain - Use domain services for complex rules
- Use Mapperly - Source-generated mapping for performance (prevents copy-paste bugs)
- WhereIf pattern - Clean optional filtering
- Structured logging - Consistent format for tracing
- Input sanitization - Trim and normalize inputs
- Authorization - Always check permissions
- Verify manual mappings - Double-check similar-named property assignments
Related Skills
abp-entity-patterns- Domain layer patternsabp-infrastructure-patterns- Cross-cutting concernsfluentvalidation-patterns- Input validation