关于 abp-api-implementation
abp-api-implementation 是面向 ABP Framework 开发者的 C# API 实现技能,涵盖 AppService、DTO、分页筛选和授权等模式。它还提供验证、映射、错误处理与路由的实现示例。
主要功能
- 01使用 AppService 实现 CRUD API
- 02设计输入与输出 DTO
- 03构建分页、排序和筛选查询
- 04配置权限与资源授权
- 05处理验证、映射和业务错误
适用场景
- 01为 ABP 应用实现 REST API
- 02创建可分页筛选的列表端点
- 03为 API 端点添加授权和验证
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ thapaliyabikendra/ ai-artifacts --skill abp-api-implementation安装后在 Claude Code 中输入/abp-api-implementation确认已加载。
详细介绍
ABP API 实现
使用 AppService、DTO、分页、筛选和授权,在 ABP Framework 中实现 REST API。此技能侧重于 C# 实现;设计原则请参阅 api-design-principles。
何时使用此技能
- 在 ABP AppService 中实现 REST API 端点
- 创建分页和筛选列表端点
- 为 API 端点设置授权
- 设计 API 请求/响应 DTO
- 处理 API 错误和验证
适用人群
- ABP 开发者——API 实现
- 后端开发者——.NET/C# 模式
设计方面:有关 API 合约设计决策,请使用
api-design-principles。
核心模式
1. 包含完整 CRUD 的 AppService
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 模式
输出 DTO(响应):
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;
}
创建 DTO(输入):
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; }
}
更新 DTO(输入):
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; }
}
列表输入 DTO(查询参数):
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 模式
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. 授权模式
基于权限的授权:
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) { ... }
}
编程式授权检查:
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
}
权限定义:
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. 使用 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. 自定义端点
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/[email protected]
[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. 接口定义(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 配置
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);
}
在 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();
}
错误处理
业务异常:
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
}
错误代码:
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";
}
本地化:
{
"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 路由
ABP 会根据 AppService 命名自动生成路由:
| 方法 | AppService 方法 | 生成的路由 |
|---|---|---|
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} |
自定义路由覆盖:
[RemoteService(Name = "PatientApi")]
[Route("api/v1/patients")] // Custom route
public class PatientAppService : ApplicationService, IPatientAppService
{
[HttpGet("{id:guid}")]
public async Task<PatientDto> GetAsync(Guid id) { ... }
}
与其他技能集成
| 需求 | 技能 |
|---|---|
| API 设计决策 | api-design-principles |
| 响应包装器 | api-response-patterns |
| 输入验证 | fluentvalidation-patterns |
| 实体设计 | abp-entity-patterns |
| 查询优化 | linq-optimization-patterns |
| 授权 | openiddict-authorization |
评论
支持 Markdown