Skip to content
cccskills
FreePublished on GitHub

abp-api-implementation

Implement REST APIs in ABP Framework with AppServices, DTOs, pagination, filtering, and authorization. Use when building API endpoints for ABP applications.

The English summary is being prepared. Showing the original description.

See how to install

Files included (2)

  • SKILL.md15.4 KB
  • source.json709 B

SKILL.md (original)

Review the instructions the agent will receive before you install.

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<Patient, Guid> patientRepository)
    {
        _patientRepository = patientRepository;
    }

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

    // GET /api/app/patient?skipCount=0&maxResultCount=10&sorting=name&filter=john
    [Authorize(MyAppPermissions.Patients.Default)]
    public async Task<PagedResultDto<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<PatientDto>(
            totalCount,
            ObjectMapper.Map<List<Patient>, List<PatientDto>>(patients)
        );
    }

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

        await _patientRepository.InsertAsync(patient);

        return ObjectMapper.Map<Patient, PatientDto>(patient);
    }

    // PUT /api/app/patient/{id}
    [Authorize(MyAppPermissions.Patients.Edit)]
    public async Task<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<Patient, PatientDto>(patient);
    }

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

2. 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; }
}

3. 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 <= 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<PatientDto>(
        totalCount,
        ObjectMapper.Map<List<Patient>, List<PatientDto>>(patients)
    );
}

4. 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<PatientDto> CreateAsync(CreatePatientDto input) { ... }

    // Edit permission
    [Authorize(MyAppPermissions.Patients.Edit)]
    public async Task<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
    }
}

5. Validation with FluentValidation

using FluentValidation;

namespace MyApp.Patients;

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

    public CreatePatientDtoValidator(IRepository<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<bool> BeUniqueEmail(string email, CancellationToken cancellationToken)
    {
        return !await _patientRepository.AnyAsync(p => p.Email == email);
    }
}

6. 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<PatientDto?> GetByEmailAsync(string email)
    {
        var patient = await _patientRepository.FirstOrDefaultAsync(p => p.Email == email);
        return patient == null ? null : ObjectMapper.Map<Patient, PatientDto>(patient);
    }

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

7. 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<PagedResultDto<PatientDto>> GetListAsync(GetPatientListInput input);

    Task<PatientDto> CreateAsync(CreatePatientDto input);

    Task<PatientDto> UpdateAsync(Guid id, UpdatePatientDto input);

    Task DeleteAsync(Guid id);

    // Custom methods
    Task<PatientDto> ActivateAsync(Guid id);

    Task<PatientDto?> GetByEmailAsync(string email);

    Task<List<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:

MethodAppService MethodGenerated 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

NeedSkill
API design decisionsapi-design-principles
Response wrappersapi-response-patterns
Input validationfluentvalidation-patterns
Entity designabp-entity-patterns
Query optimizationlinq-optimization-patterns
Authorizationopeniddict-authorization

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

007

Free

Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Guides the creation of agile user stories and Gherkin feature files. Use when the user wants to create a user story, write acceptance criteria, define Gherkin scenarios, or author BDD feature files. This should trigger for requests such as Create a user story; Write a user story; I need to write a user story. Part of cursor-rules-java project

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Facilitates conversational discovery to create Architectural Decision Records (ADRs) for non-functional requirements using the ISO/IEC 25010:2023 quality model. Use when the user wants to document quality attributes, NFR decisions, security/performance/scalability architecture, or design systems with measurable quality criteria. This should trigger for requests such as Create ADR for Non-functional requirements; Document Non-functional requirements; Capture Non-functional requirements; Generate Non-functional requirements in an ADR. Part of cursor-rules-java project

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Run a health check on an existing project: dependency audit, security scan, test runner detection, CI/CD evaluation, and missing configuration analysis. Maps the three execution gates (pre/in/post) from /10x-bootstrapper to an assessment framework for existing codebases. Reads optional context/foundation/stack-assessment.md from /10x-stack-assess to focus checks on identified gaps. Writes context/foundation/health-check.md with findings, prioritized fixes, and an agent-readiness verdict. Use when the user has an existing project and wants to verify its health before working with an agent. Trigger phrases: "health check", "check my project", "audit my project", "is my project healthy", "sprawdź projekt", "audyt projektu", "health-check", "project health". Use AFTER /10x-stack-assess (brownfield chain), BEFORE agent onboarding (m1-l4).

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

You MUST use this when building projects end-to-end. Orchestrates all 12 team roles — automatically switches between CTO, architect, PM, engineers, SRE, security, DBA, QA, and EM based on the current phase of work. Starts with brainstorming before any implementation.

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Use when you need to add or configure Maven plugins in your pom.xml — including quality tools (enforcer, surefire, failsafe, jacoco, pitest, spotbugs, pmd), security scanning (OWASP), code formatting (Spotless), version management, container image build (Jib), build information tracking, and benchmarking (JMH) — through a consultative, modular step-by-step approach that only adds what you actually need. This should trigger for requests such as Add Maven plugins in pom.xml; Improve Maven plugins in pom.xml. Part of cursor-rules-java project

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

See all skills by aibot88

Report a problem with this skill