🎯

abp-api-implementation

🎯Skill

from thapaliyabikendra/ai-artifacts

VibeIndex|
What it does

Implements comprehensive REST APIs in ABP Framework with robust AppServices, DTOs, pagination, filtering, and authorization for .NET applications.

abp-api-implementation

Installation

Install skill:
npx skills add https://github.com/thapaliyabikendra/ai-artifacts --skill abp-api-implementation
2
AddedJan 27, 2026

Skill Details

SKILL.md

"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 _patientRepository;

public PatientAppService(IRepository patientRepository)

{

_patientRepository = patientRepository;

}

// GET /api/app/patient/{id}

[Authorize(MyAppPermissions.Patients.Default)]

public async Task GetAsync(Guid id)

{

var patient = await _patientRepository.GetAsync(id);

return ObjectMapper.Map(patient);

}

// GET /api/app/patient?skipCount=0&maxResultCount=10&sorting=name&filter=john

[Authorize(MyAppPermissions.Patients.Default)]

public async Task> 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(

totalCount,

ObjectMapper.Map, List>(patients)

);

}

// POST /api/app/patient

[Authorize(MyAppPermissions.Patients.Create)]

public async Task CreateAsync(CreatePatientDto input)

{

var patient = new Patient(

GuidGenerator.Create(),

input.Name,

input.Email,

input.DateOfBirth

);

await _patientRepository.InsertAsync(patient);

return ObjectMapper.Map(patient);

}

// PUT /api/app/patient/{id}

[Authorize(MyAppPermissions.Patients.Edit)]

public async Task 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);

}

// 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> 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(

totalCount,

ObjectMapper.Map, List>(patients)

);

}

```

4. Authorization Patterns

Permission-Based Authorization:

```csharp

public class PatientAppService : ApplicationService, IPatientAppService

{

// Read permission

[Authorize(MyAppPermissions.Patients.Default)]

public async Task GetAsync(Guid id) { ... }

// Create permission

[Authorize(MyAppPermissions.Patients.Create)]

public async Task CreateAsync(CreatePatientDto input) { ... }

// Edit permission

[Authorize(MyAppPermissions.Patients.Edit)]

public async Task 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 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

{

private readonly IRepository _patientRepository;

public CreatePatientDtoValidator(IRepository 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 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 ActivateAsync(Guid id)

{

var patient = await _patientRepository.GetAsync(id);

patient.Activate();

await _patientRepository.UpdateAsync(patient);

return ObjectMapper.Map(patient);

}

// Custom query: GET /api/app/patient/by-email?email=john@example.com

[HttpGet("by-email")]

[Authorize(MyAppPermissions.Patients.Default)]

public async Task GetByEmailAsync(string email)

{

var patient = await _patientRepository.FirstOrDefaultAsync(p => p.Email == email);

return patient == null ? null : ObjectMapper.Map(patient);

}

// 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 GetAsync(Guid id);

Task> GetListAsync(GetPatientListInput input);

Task CreateAsync(CreatePatientDto input);

Task UpdateAsync(Guid id, UpdatePatientDto input);

Task DeleteAsync(Guid id);

// Custom methods

Task ActivateAsync(Guid id);

Task GetByEmailAsync(string email);

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 ToDtoList(this List 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 CreateAsync(CreatePatientDto input)

{

var patient = input.ToEntity();

patient.Id = GuidGenerator.Create();

await _patientRepository.InsertAsync(patient);

return patient.ToDto();

}

public async Task 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 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 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 |

More from this repository10

🎯
clean-code-dotnet🎯Skill

Provides clean code guidelines and refactoring techniques for C#/.NET, focusing on improving code readability, maintainability, and adherence to SOLID principles.

🎯
abp-entity-patterns🎯Skill

Implements domain layer patterns for ABP Framework, providing robust entity, aggregate, repository, and domain service implementations following DDD principles.

🎯
abp-infrastructure-patterns🎯Skill

abp-infrastructure-patterns skill from thapaliyabikendra/ai-artifacts

🎯
efcore-patterns🎯Skill

Configures and optimizes Entity Framework Core patterns for ABP Framework, focusing on entity configuration, migrations, and relationship design with PostgreSQL.

🎯
fluentvalidation-patterns🎯Skill

Validates input DTOs in ABP Framework using FluentValidation with async checks, conditional rules, custom validators, and localized error messages.

🎯
claude-artifact-creator🎯Skill

Generates Claude Code artifacts like skills, agents, and commands with best practices and quality standards.

🎯
content-retrieval🎯Skill

content-retrieval skill from thapaliyabikendra/ai-artifacts

🎯
system-design-patterns🎯Skill

Designs scalable, reliable distributed systems by applying proven architectural patterns and evaluating trade-offs across performance, consistency, and availability.

🎯
abp-contract-scaffolding🎯Skill

Generates ABP Application.Contracts layer scaffolding, enabling parallel development by creating standardized interfaces, DTOs, and permissions for .NET microservices.

🎯
openiddict-authorization🎯Skill

Implements permission-based OAuth 2.0 authorization for ABP Framework using OpenIddict, enabling fine-grained access control and multi-tenant security.