abp-api-implementation
Implement REST APIs in ABP Framework with AppServices, DTOs, pagination, filtering, and authorization. Use when building API endpoints for ABP applications.
Best use case
abp-api-implementation is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Implement REST APIs in ABP Framework with AppServices, DTOs, pagination, filtering, and authorization. Use when building API endpoints for ABP applications.
Teams using abp-api-implementation should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/abp-api-implementation/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How abp-api-implementation Compares
| Feature / Agent | abp-api-implementation | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Implement REST APIs in ABP Framework with AppServices, DTOs, pagination, filtering, and authorization. Use when building API endpoints for ABP applications.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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
```csharp
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):
```csharp
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):
```csharp
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):
```csharp
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):
```csharp
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
```csharp
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**:
```csharp
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**:
```csharp
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**:
```csharp
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
```csharp
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
```csharp
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)
```csharp
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
```csharp
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:
```csharp
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**:
```csharp
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**:
```csharp
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**:
```json
{
"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**:
```csharp
[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` |Related Skills
advanced-agentdb-vector-search-implementation
Advanced AgentDB Vector Search Implementation operates on 3 fundamental principles:
ai-agent-implementation
Step-by-step checklist and best practices for implementing new AI agent tools in the omer-akben portfolio. Use when creating new agent tools, API routes, or extending agent capabilities.
tech-blog
Generates comprehensive technical blog posts, offering detailed explanations of system internals, architecture, and implementation, either through source code analysis or document-driven research.
whisper-transcribe
Transcribes audio and video files to text using OpenAI's Whisper CLI, enhanced with contextual grounding from local markdown files for improved accuracy.
ontopo
An AI agent skill to search for Israeli restaurants, check table availability, view menus, and retrieve booking links via the Ontopo platform, acting as an unofficial interface to its data.
vly-money
Generate crypto payment links for supported tokens and networks, manage access to X402 payment-protected content, and provide direct access to the vly.money wallet interface.
grail-miner
This skill assists in setting up, managing, and optimizing Grail miners on Bittensor Subnet 81, handling tasks like environment configuration, R2 storage, model checkpoint management, and performance tuning.
chrome-debug
This skill empowers AI agents to debug web applications and inspect browser behavior using the Chrome DevTools Protocol (CDP), offering both collaborative (headful) and automated (headless) modes.
ux
This AI agent skill provides comprehensive guidance for creating professional and insightful User Experience (UX) designs, covering user research, information architecture, interaction design, visual guidance, and usability evaluation. It aims to produce actionable, user-centered solutions that avoid generic AI aesthetics.
modal-deployment
Run Python code in the cloud with serverless containers, GPUs, and autoscaling using Modal. This skill enables agents to generate code for deploying ML models, running batch jobs, serving APIs, and scaling compute-intensive workloads.
lets-go-rss
A lightweight, full-platform RSS subscription manager that aggregates content from YouTube, Vimeo, Behance, Twitter/X, and Chinese platforms like Bilibili, Weibo, and Douyin, featuring deduplication and AI smart classification.
astro
This skill provides essential Astro framework patterns, focusing on server-side rendering (SSR), static site generation (SSG), middleware, and TypeScript best practices. It helps AI agents implement secure authentication, manage API routes, and debug rendering behaviors within Astro projects.