abp-service-patterns
π―Skillfrom thapaliyabikendra/ai-artifacts
Enables rapid development of ABP application services with standardized patterns for creating, mapping, filtering, and managing DTOs using Mapperly and common service implementations.
Installation
npx skills add https://github.com/thapaliyabikendra/ai-artifacts --skill abp-service-patternsSkill Details
"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."
Overview
# ABP Service Patterns
Application layer patterns for ABP Framework.
Application Service Pattern
```csharp
public class PatientAppService : ApplicationService, IPatientAppService
{
private readonly IRepository
private readonly PatientManager _patientManager; // Domain service
private readonly ClinicApplicationMappers _mapper;
public PatientAppService(
IRepository
PatientManager patientManager,
ClinicApplicationMappers mapper)
{
_patientRepository = patientRepository;
_patientManager = patientManager;
_mapper = mapper;
}
[Authorize(ClinicPermissions.Patients.Default)]
public async Task
{
var patient = await _patientRepository.GetAsync(id);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Create)]
public async Task
{
var patient = await _patientManager.CreateAsync(
input.FirstName, input.LastName, input.Email, input.DateOfBirth);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Edit)]
public async Task
{
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.
```csharp
// Application/ClinicApplicationMappers.cs
[Mapper]
public partial class ClinicApplicationMappers
{
// Entity to DTO
public partial PatientDto PatientToDto(Patient patient);
public partial List
// 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:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton
}
```
Unit of Work
ABP automatically manages UoW for application service methods.
```csharp
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
{
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:
```csharp
[UnitOfWork(isTransactional: false)] // Disable for read-only
public async Task GenerateLargeReportAsync() { }
public async Task ProcessBatchAsync(List
{
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:
```csharp
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:
```csharp
public async Task
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
}
```
ResponseModel Wrapper
```csharp
public class ResponseModel
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public static ResponseModel
=> new() { IsSuccess = true, Data = data, Message = message };
public static ResponseModel
=> new() { IsSuccess = false, Message = message };
}
// Usage
public async Task
{
var patient = await _patientRepository.FirstOrDefaultAsync(x => x.Id == id);
if (patient == null)
return ResponseModel
return ResponseModel
}
```
CommonDependencies Pattern
Reduce constructor bloat by grouping cross-cutting dependencies.
```csharp
public class CommonDependencies
{
public IDistributedEventBus DistributedEventBus { get; set; }
public IDataFilter DataFilter { get; set; }
public ILogger
public IGuidGenerator GuidGenerator { get; set; }
}
// Register
context.Services.AddTransient(typeof(CommonDependencies<>));
// Usage
public class PatientAppService : ApplicationService
{
private readonly IRepository
private readonly CommonDependencies
public PatientAppService(
IRepository
CommonDependencies
{
_patientRepository = patientRepository;
_common = common;
}
public async Task
{
_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
```csharp
public async Task
{
_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
```csharp
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
{
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:
```csharp
// β 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
More from this repository10
Provides clean code guidelines and refactoring techniques for C#/.NET, focusing on improving code readability, maintainability, and adherence to SOLID principles.
Configures and optimizes Entity Framework Core patterns for ABP Framework, focusing on entity configuration, migrations, and relationship design with PostgreSQL.
Implements comprehensive REST APIs in ABP Framework with robust AppServices, DTOs, pagination, filtering, and authorization for .NET applications.
Implements domain layer patterns for ABP Framework, providing robust entity, aggregate, repository, and domain service implementations following DDD principles.
Validates input DTOs in ABP Framework using FluentValidation with async checks, conditional rules, custom validators, and localized error messages.
abp-infrastructure-patterns skill from thapaliyabikendra/ai-artifacts
content-retrieval skill from thapaliyabikendra/ai-artifacts
Designs scalable, reliable distributed systems by applying proven architectural patterns and evaluating trade-offs across performance, consistency, and availability.
Generates ABP Application.Contracts layer scaffolding, enabling parallel development by creating standardized interfaces, DTOs, and permissions for .NET microservices.
Implements permission-based OAuth 2.0 authorization for ABP Framework using OpenIddict, enabling fine-grained access control and multi-tenant security.