abp-service-patterns

Application layer patterns for ABP Framework.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "abp-service-patterns" with this command: npx skills add thapaliyabikendra/ai-artifacts/thapaliyabikendra-ai-artifacts-abp-service-patterns

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&#x3C;Patient, Guid> patientRepository,
    PatientManager patientManager,
    ClinicApplicationMappers mapper)
{
    _patientRepository = patientRepository;
    _patientManager = patientManager;
    _mapper = mapper;
}

[Authorize(ClinicPermissions.Patients.Default)]
public async Task&#x3C;PatientDto> GetAsync(Guid id)
{
    var patient = await _patientRepository.GetAsync(id);
    return _mapper.PatientToDto(patient);
}

[Authorize(ClinicPermissions.Patients.Create)]
public async Task&#x3C;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&#x3C;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 &#x3C;= 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&#x3C;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&#x3C;T> Success(T data, string message = null)
    => new() { IsSuccess = true, Data = data, Message = message };

public static ResponseModel&#x3C;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&#x3C;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&#x3C;Patient, Guid> patientRepository,
    CommonDependencies&#x3C;PatientAppService> common)
{
    _patientRepository = patientRepository;
    _common = common;
}

public async Task&#x3C;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., IsXxxCompleted vs 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

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 patterns

  • abp-infrastructure-patterns

  • Cross-cutting concerns

  • fluentvalidation-patterns

  • Input validation

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

abp-infrastructure-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

abp-entity-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

abp-api-implementation

No summary provided by upstream source.

Repository SourceNeeds Review
General

abp-contract-scaffolding

No summary provided by upstream source.

Repository SourceNeeds Review