mcpField
A
abp-api-implementation

abp-api-implementation

github.com/thapaliyabikendra開発2458 回インストール31 週間前に更新
リポジトリ

abp-api-implementation について

abp-api-implementationは、ABP Frameworkを使う開発者向けのC# API実装スキルです。AppService、DTO、ページネーション、フィルタリング、認可に加え、検証やエラー処理の実装パターンを扱います。

主な機能

  1. 01AppServiceによるCRUD APIの実装
  2. 02入出力DTOの設計
  3. 03ページネーション、ソート、絞り込みクエリ
  4. 04権限認可とリソース認可の設定
  5. 05検証、マッピング、業務エラーの処理

活用シーン

  1. 01ABPアプリケーションでのREST API実装
  2. 02ページ分割・絞り込み対応リストの作成
  3. 03APIエンドポイントへの認可と検証の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thapaliyabikendra/ai-artifacts --skill abp-api-implementation

インストール後、Claude Code で/abp-api-implementationを入力して読み込まれたか確認します。

詳細

ABP API の実装

AppServices、DTO、ページネーション、フィルタリング、認可を使用して、ABP Framework で REST API を実装します。このスキルはC# の実装に重点を置いています。設計原則については、api-design-principles を参照してください。

このスキルを使用する場面

  • ABP AppServices で 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 の名前に基づいてルートを自動生成します。

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}

カスタムルートのオーバーライド:

[RemoteService(Name = "PatientApi")]
[Route("api/v1/patients")] // Custom route
public class PatientAppService : ApplicationService, IPatientAppService
{
    [HttpGet("{id:guid}")]
    public async Task<PatientDto> GetAsync(Guid id) { ... }
}

他のスキルとの連携

NeedSkill
API 設計の判断api-design-principles
レスポンスラッパーapi-response-patterns
入力バリデーションfluentvalidation-patterns
エンティティ設計abp-entity-patterns
クエリの最適化linq-optimization-patterns
認可openiddict-authorization

コメント