abp-api-implementation
π―Skillfrom thapaliyabikendra/ai-artifacts
Implements comprehensive REST APIs in ABP Framework with robust AppServices, DTOs, pagination, filtering, and authorization for .NET applications.
Installation
npx skills add https://github.com/thapaliyabikendra/ai-artifacts --skill abp-api-implementationSkill Details
"Implement REST APIs in ABP Framework with AppServices, DTOs, pagination, filtering, and authorization. Use when building API endpoints for ABP applications."
Overview
# 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
public PatientAppService(IRepository
{
_patientRepository = patientRepository;
}
// GET /api/app/patient/{id}
[Authorize(MyAppPermissions.Patients.Default)]
public async Task
{
var patient = await _patientRepository.GetAsync(id);
return ObjectMapper.Map
}
// GET /api/app/patient?skipCount=0&maxResultCount=10&sorting=name&filter=john
[Authorize(MyAppPermissions.Patients.Default)]
public async Task
{
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
totalCount,
ObjectMapper.Map, List
);
}
// POST /api/app/patient
[Authorize(MyAppPermissions.Patients.Create)]
public async Task
{
var patient = new Patient(
GuidGenerator.Create(),
input.Name,
input.Email,
input.DateOfBirth
);
await _patientRepository.InsertAsync(patient);
return ObjectMapper.Map
}
// PUT /api/app/patient/{id}
[Authorize(MyAppPermissions.Patients.Edit)]
public async Task
{
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
}
// 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
{
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
{
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
totalCount,
ObjectMapper.Map, List
);
}
```
4. Authorization Patterns
Permission-Based Authorization:
```csharp
public class PatientAppService : ApplicationService, IPatientAppService
{
// Read permission
[Authorize(MyAppPermissions.Patients.Default)]
public async Task
// Create permission
[Authorize(MyAppPermissions.Patients.Create)]
public async Task
// Edit permission
[Authorize(MyAppPermissions.Patients.Edit)]
public async Task
// Delete permission (often more restricted)
[Authorize(MyAppPermissions.Patients.Delete)]
public async Task DeleteAsync(Guid id) { ... }
}
```
Programmatic Authorization Check:
```csharp
public async Task
{
// 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
{
private readonly IRepository
public CreatePatientDtoValidator(IRepository
{
_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
{
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
{
var patient = await _patientRepository.GetAsync(id);
patient.Activate();
await _patientRepository.UpdateAsync(patient);
return ObjectMapper.Map
}
// Custom query: GET /api/app/patient/by-email?email=john@example.com
[HttpGet("by-email")]
[Authorize(MyAppPermissions.Patients.Default)]
public async Task
{
var patient = await _patientRepository.FirstOrDefaultAsync(p => p.Email == email);
return patient == null ? null : ObjectMapper.Map
}
// Custom query with lookup data: GET /api/app/patient/lookup
[HttpGet("lookup")]
[Authorize(MyAppPermissions.Patients.Default)]
public async Task> 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
Task
Task
Task
Task DeleteAsync(Guid id);
// Custom methods
Task
Task
Task> 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
// 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
{
var patient = input.ToEntity();
patient.Id = GuidGenerator.Create();
await _patientRepository.InsertAsync(patient);
return patient.ToDto();
}
public async Task
{
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
{
// 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
}
```
---
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 |
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.
Implements domain layer patterns for ABP Framework, providing robust entity, aggregate, repository, and domain service implementations following DDD principles.
abp-infrastructure-patterns skill from thapaliyabikendra/ai-artifacts
Configures and optimizes Entity Framework Core patterns for ABP Framework, focusing on entity configuration, migrations, and relationship design with PostgreSQL.
Validates input DTOs in ABP Framework using FluentValidation with async checks, conditional rules, custom validators, and localized error messages.
Generates Claude Code artifacts like skills, agents, and commands with best practices and quality standards.
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.