Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/abp-service-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-abp-service-patterns && rm -rf "$T"
skills/data/abp-service-patterns/SKILL.mdABP 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.,
vsIsXxxCompleted
)IsYyyCompleted - 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 |
|---|---|---|
| | |
| | |
| | |
| | |
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
- Domain layer patternsabp-entity-patterns
- Cross-cutting concernsabp-infrastructure-patterns
- Input validationfluentvalidation-patterns