abp-api-implementation

ABP API Implementation

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-api-implementation" with this command: npx skills add thapaliyabikendra/ai-artifacts/thapaliyabikendra-ai-artifacts-abp-api-implementation

ABP API Implementation

Implement REST APIs in ABP Framework using AppServices, DTOs, pagination, filtering, and authorization. This skill focuses on C# implementation - for design principles, see api-design-principles .

When to Use This Skill

  • Implementing REST API endpoints in ABP AppServices

  • Creating paginated and filtered list endpoints

  • Setting up authorization on API endpoints

  • Designing DTOs for API requests/responses

  • Handling API errors and validation

Audience

  • ABP Developers - API implementation

  • Backend Developers - .NET/C# patterns

For Design: Use api-design-principles for API contract design decisions.

Core Patterns

  1. AppService with Full CRUD

using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories;

namespace MyApp.Patients;

public class PatientAppService : ApplicationService, IPatientAppService { private readonly IRepository<Patient, Guid> _patientRepository;

public PatientAppService(IRepository&#x3C;Patient, Guid> patientRepository)
{
    _patientRepository = patientRepository;
}

// GET /api/app/patient/{id}
[Authorize(MyAppPermissions.Patients.Default)]
public async Task&#x3C;PatientDto> GetAsync(Guid id)
{
    var patient = await _patientRepository.GetAsync(id);
    return ObjectMapper.Map&#x3C;Patient, PatientDto>(patient);
}

// GET /api/app/patient?skipCount=0&#x26;maxResultCount=10&#x26;sorting=name&#x26;filter=john
[Authorize(MyAppPermissions.Patients.Default)]
public async Task&#x3C;PagedResultDto&#x3C;PatientDto>> GetListAsync(GetPatientListInput input)
{
    var query = await _patientRepository.GetQueryableAsync();

    // Apply filters using WhereIf pattern
    query = query
        .WhereIf(!input.Filter.IsNullOrWhiteSpace(),
            p => p.Name.Contains(input.Filter!) ||
                 p.Email.Contains(input.Filter!))
        .WhereIf(input.Status.HasValue,
            p => p.Status == input.Status!.Value)
        .WhereIf(input.DoctorId.HasValue,
            p => p.DoctorId == input.DoctorId!.Value);

    // Get total count before pagination
    var totalCount = await AsyncExecuter.CountAsync(query);

    // Apply sorting and pagination
    query = query
        .OrderBy(input.Sorting.IsNullOrWhiteSpace() ? nameof(Patient.Name) : input.Sorting)
        .PageBy(input);

    var patients = await AsyncExecuter.ToListAsync(query);

    return new PagedResultDto&#x3C;PatientDto>(
        totalCount,
        ObjectMapper.Map&#x3C;List&#x3C;Patient>, List&#x3C;PatientDto>>(patients)
    );
}

// POST /api/app/patient
[Authorize(MyAppPermissions.Patients.Create)]
public async Task&#x3C;PatientDto> CreateAsync(CreatePatientDto input)
{
    var patient = new Patient(
        GuidGenerator.Create(),
        input.Name,
        input.Email,
        input.DateOfBirth
    );

    await _patientRepository.InsertAsync(patient);

    return ObjectMapper.Map&#x3C;Patient, PatientDto>(patient);
}

// PUT /api/app/patient/{id}
[Authorize(MyAppPermissions.Patients.Edit)]
public async Task&#x3C;PatientDto> UpdateAsync(Guid id, UpdatePatientDto input)
{
    var patient = await _patientRepository.GetAsync(id);

    patient.SetName(input.Name);
    patient.SetEmail(input.Email);
    patient.SetDateOfBirth(input.DateOfBirth);

    await _patientRepository.UpdateAsync(patient);

    return ObjectMapper.Map&#x3C;Patient, PatientDto>(patient);
}

// DELETE /api/app/patient/{id}
[Authorize(MyAppPermissions.Patients.Delete)]
public async Task DeleteAsync(Guid id)
{
    await _patientRepository.DeleteAsync(id);
}

}

  1. DTO Patterns

Output DTO (Response):

using Volo.Abp.Application.Dtos;

namespace MyApp.Patients;

public class PatientDto : FullAuditedEntityDto<Guid> { public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public DateTime DateOfBirth { get; set; } public PatientStatus Status { get; set; } public Guid? DoctorId { get; set; }

// Computed property
public int Age => DateTime.Today.Year - DateOfBirth.Year;

}

Create DTO (Input):

namespace MyApp.Patients;

public class CreatePatientDto { public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public DateTime DateOfBirth { get; set; } public Guid? DoctorId { get; set; } }

Update DTO (Input):

namespace MyApp.Patients;

public class UpdatePatientDto { public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public DateTime DateOfBirth { get; set; } public PatientStatus Status { get; set; } }

List Input DTO (Query Parameters):

using Volo.Abp.Application.Dtos;

namespace MyApp.Patients;

public class GetPatientListInput : PagedAndSortedResultRequestDto { // Search filter public string? Filter { get; set; }

// Specific filters
public PatientStatus? Status { get; set; }
public Guid? DoctorId { get; set; }
public DateTime? CreatedAfter { get; set; }
public DateTime? CreatedBefore { get; set; }

}

  1. WhereIf Pattern for Filtering

using System.Linq.Dynamic.Core;

public async Task<PagedResultDto<PatientDto>> GetListAsync(GetPatientListInput input) { var query = await _patientRepository.GetQueryableAsync();

// WhereIf - only applies condition if value is not null/empty
query = query
    // Text search
    .WhereIf(!input.Filter.IsNullOrWhiteSpace(),
        p => p.Name.Contains(input.Filter!) ||
             p.Email.Contains(input.Filter!) ||
             p.PhoneNumber.Contains(input.Filter!))

    // Enum filter
    .WhereIf(input.Status.HasValue,
        p => p.Status == input.Status!.Value)

    // Foreign key filter
    .WhereIf(input.DoctorId.HasValue,
        p => p.DoctorId == input.DoctorId!.Value)

    // Date range filter
    .WhereIf(input.CreatedAfter.HasValue,
        p => p.CreationTime >= input.CreatedAfter!.Value)
    .WhereIf(input.CreatedBefore.HasValue,
        p => p.CreationTime &#x3C;= input.CreatedBefore!.Value)

    // Boolean filter
    .WhereIf(input.IsActive.HasValue,
        p => p.IsActive == input.IsActive!.Value);

var totalCount = await AsyncExecuter.CountAsync(query);

// Dynamic sorting with System.Linq.Dynamic.Core
var sorting = input.Sorting.IsNullOrWhiteSpace()
    ? $"{nameof(Patient.CreationTime)} DESC"
    : input.Sorting;

query = query.OrderBy(sorting).PageBy(input);

var patients = await AsyncExecuter.ToListAsync(query);

return new PagedResultDto&#x3C;PatientDto>(
    totalCount,
    ObjectMapper.Map&#x3C;List&#x3C;Patient>, List&#x3C;PatientDto>>(patients)
);

}

  1. Authorization Patterns

Permission-Based Authorization:

public class PatientAppService : ApplicationService, IPatientAppService { // Read permission [Authorize(MyAppPermissions.Patients.Default)] public async Task<PatientDto> GetAsync(Guid id) { ... }

// Create permission
[Authorize(MyAppPermissions.Patients.Create)]
public async Task&#x3C;PatientDto> CreateAsync(CreatePatientDto input) { ... }

// Edit permission
[Authorize(MyAppPermissions.Patients.Edit)]
public async Task&#x3C;PatientDto> UpdateAsync(Guid id, UpdatePatientDto input) { ... }

// Delete permission (often more restricted)
[Authorize(MyAppPermissions.Patients.Delete)]
public async Task DeleteAsync(Guid id) { ... }

}

Programmatic Authorization Check:

public async Task<PatientDto> UpdateAsync(Guid id, UpdatePatientDto input) { // Check permission programmatically await AuthorizationService.CheckAsync(MyAppPermissions.Patients.Edit);

var patient = await _patientRepository.GetAsync(id);

// Resource-based authorization
if (patient.DoctorId != CurrentUser.Id)
{
    await AuthorizationService.CheckAsync(MyAppPermissions.Patients.EditAny);
}

// ... update logic

}

Permission Definitions:

public static class MyAppPermissions { public const string GroupName = "MyApp";

public static class Patients
{
    public const string Default = GroupName + ".Patients";
    public const string Create = Default + ".Create";
    public const string Edit = Default + ".Edit";
    public const string Delete = Default + ".Delete";
    public const string EditAny = Default + ".EditAny"; // Admin only
}

}

  1. Validation with FluentValidation

using FluentValidation;

namespace MyApp.Patients;

public class CreatePatientDtoValidator : AbstractValidator<CreatePatientDto> { private readonly IRepository<Patient, Guid> _patientRepository;

public CreatePatientDtoValidator(IRepository&#x3C;Patient, Guid> patientRepository)
{
    _patientRepository = patientRepository;

    RuleFor(x => x.Name)
        .NotEmpty().WithMessage("Name is required.")
        .MaximumLength(100).WithMessage("Name cannot exceed 100 characters.");

    RuleFor(x => x.Email)
        .NotEmpty().WithMessage("Email is required.")
        .EmailAddress().WithMessage("Invalid email format.")
        .MustAsync(BeUniqueEmail).WithMessage("Email already exists.");

    RuleFor(x => x.DateOfBirth)
        .NotEmpty().WithMessage("Date of birth is required.")
        .LessThan(DateTime.Today).WithMessage("Date of birth must be in the past.")
        .GreaterThan(DateTime.Today.AddYears(-150)).WithMessage("Invalid date of birth.");
}

private async Task&#x3C;bool> BeUniqueEmail(string email, CancellationToken cancellationToken)
{
    return !await _patientRepository.AnyAsync(p => p.Email == email);
}

}

  1. Custom Endpoints

public class PatientAppService : ApplicationService, IPatientAppService { // Custom action: POST /api/app/patient/{id}/activate [HttpPost("{id}/activate")] [Authorize(MyAppPermissions.Patients.Edit)] public async Task<PatientDto> ActivateAsync(Guid id) { var patient = await _patientRepository.GetAsync(id); patient.Activate(); await _patientRepository.UpdateAsync(patient); return ObjectMapper.Map<Patient, PatientDto>(patient); }

// Custom query: GET /api/app/patient/by-email?email=john@example.com
[HttpGet("by-email")]
[Authorize(MyAppPermissions.Patients.Default)]
public async Task&#x3C;PatientDto?> GetByEmailAsync(string email)
{
    var patient = await _patientRepository.FirstOrDefaultAsync(p => p.Email == email);
    return patient == null ? null : ObjectMapper.Map&#x3C;Patient, PatientDto>(patient);
}

// Custom query with lookup data: GET /api/app/patient/lookup
[HttpGet("lookup")]
[Authorize(MyAppPermissions.Patients.Default)]
public async Task&#x3C;List&#x3C;PatientLookupDto>> GetLookupAsync()
{
    var patients = await _patientRepository.GetListAsync();
    return patients.Select(p => new PatientLookupDto
    {
        Id = p.Id,
        DisplayName = $"{p.Name} ({p.Email})"
    }).ToList();
}

}

  1. Interface Definition (Application.Contracts)

using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services;

namespace MyApp.Patients;

public interface IPatientAppService : IApplicationService { Task<PatientDto> GetAsync(Guid id);

Task&#x3C;PagedResultDto&#x3C;PatientDto>> GetListAsync(GetPatientListInput input);

Task&#x3C;PatientDto> CreateAsync(CreatePatientDto input);

Task&#x3C;PatientDto> UpdateAsync(Guid id, UpdatePatientDto input);

Task DeleteAsync(Guid id);

// Custom methods
Task&#x3C;PatientDto> ActivateAsync(Guid id);

Task&#x3C;PatientDto?> GetByEmailAsync(string email);

Task&#x3C;List&#x3C;PatientLookupDto>> GetLookupAsync();

}

Mapperly Configuration

using Riok.Mapperly.Abstractions;

namespace MyApp;

[Mapper] public static partial class ApplicationMappers { // Entity to DTO public static partial PatientDto ToDto(this Patient patient); public static partial List<PatientDto> ToDtoList(this List<Patient> patients);

// DTO to Entity (for creation)
public static partial Patient ToEntity(this CreatePatientDto dto);

// Update Entity from DTO
public static partial void UpdateFrom(this Patient patient, UpdatePatientDto dto);

}

Usage in AppService:

public async Task<PatientDto> CreateAsync(CreatePatientDto input) { var patient = input.ToEntity(); patient.Id = GuidGenerator.Create();

await _patientRepository.InsertAsync(patient);

return patient.ToDto();

}

public async Task<PatientDto> UpdateAsync(Guid id, UpdatePatientDto input) { var patient = await _patientRepository.GetAsync(id); patient.UpdateFrom(input); await _patientRepository.UpdateAsync(patient); return patient.ToDto(); }

Error Handling

Business Exception:

using Volo.Abp;

public async Task<PatientDto> CreateAsync(CreatePatientDto input) { // Check business rule if (await _patientRepository.AnyAsync(p => p.Email == input.Email)) { throw new BusinessException(MyAppDomainErrorCodes.PatientEmailAlreadyExists) .WithData("email", input.Email); }

// ... create logic

}

Error Codes:

public static class MyAppDomainErrorCodes { public const string PatientEmailAlreadyExists = "MyApp:Patient:001"; public const string PatientNotActive = "MyApp:Patient:002"; public const string PatientCannotBeDeleted = "MyApp:Patient:003"; }

Localization:

{ "MyApp:Patient:001": "A patient with email '{email}' already exists.", "MyApp:Patient:002": "Patient is not active.", "MyApp:Patient:003": "Patient cannot be deleted because they have active appointments." }

API Routes

ABP auto-generates routes based on AppService naming:

Method AppService Method Generated Route

GetAsync(Guid id)

GET /api/app/patient/{id}

GetListAsync(input)

GET /api/app/patient

CreateAsync(input)

POST /api/app/patient

UpdateAsync(id, input)

PUT /api/app/patient/{id}

DeleteAsync(id)

DELETE /api/app/patient/{id}

Custom Route Override:

[RemoteService(Name = "PatientApi")] [Route("api/v1/patients")] // Custom route public class PatientAppService : ApplicationService, IPatientAppService { [HttpGet("{id:guid}")] public async Task<PatientDto> GetAsync(Guid id) { ... } }

Integration with Other Skills

Need Skill

API design decisions api-design-principles

Response wrappers api-response-patterns

Input validation fluentvalidation-patterns

Entity design abp-entity-patterns

Query optimization linq-optimization-patterns

Authorization openiddict-authorization

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-service-patterns

No summary provided by upstream source.

Repository SourceNeeds Review