abp-entity-patterns について
ABP Framework と DDD でアプリケーションを構築する開発者向けのドメイン層リファレンスです。エンティティ、リポジトリ、ドメインサービス、データ投入に加え、監査とマルチテナントのパターンを扱います。
主な機能
- 01エンティティと集約ルートの基底クラスを選択
- 02監査と論理削除のパターンを適用
- 03マルチテナントエンティティをモデル化
- 04カスタムリポジトリとドメインサービスを定義
- 05アプリケーションデータとテストデータを投入
活用シーン
- 01状態と検証をカプセル化したエンティティを設計
- 02変更を追跡し論理削除済みレコードを除外
- 03エンティティにテナント対応の動作を追加
- 04エンティティ間の業務ロジックと独自クエリを実装
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ thapaliyabikendra/ ai-artifacts --skill abp-entity-patternsインストール後、Claude Code で/abp-entity-patternsを入力して読み込まれたか確認します。
詳細
ABP エンティティパターン
DDD の原則に従った ABP Framework のドメインレイヤーパターンです。
アーキテクチャレイヤー
Domain.Shared → Constants, enums, shared types
Domain → Entities, repositories, domain services, domain events
Application.Contracts → DTOs, application service interfaces
Application → Application services, mapper profiles
EntityFrameworkCore → DbContext, repository implementations
HttpApi → Controllers
HttpApi.Host → Startup, configuration
重要な原則:依存関係は下位に向かいます。アプリケーションはドメインに依存しますが、ドメインがアプリケーションに依存することはありません。
エンティティの基底クラス
適切な基底クラスの選択
| 基底クラス | 使用する場面 |
|---|---|
Entity<TKey> | 監査が不要なシンプルなエンティティ |
AuditedEntity<TKey> | 作成・変更の追跡が必要な場合 |
FullAuditedEntity<TKey> | 論理削除と完全な監査が必要な場合 |
AggregateRoot<TKey> | 集約のルートエンティティ |
FullAuditedAggregateRoot<TKey> | 最も一般的 - すべての機能を備える |
標準的なエンティティパターン
public class Patient : FullAuditedAggregateRoot<Guid>
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Email { get; private set; }
public DateTime DateOfBirth { get; private set; }
public bool IsActive { get; private set; }
// Required for EF Core
protected Patient() { }
// Constructor with validation
public Patient(
Guid id,
string firstName,
string lastName,
string email,
DateTime dateOfBirth)
: base(id)
{
SetName(firstName, lastName);
SetEmail(email);
DateOfBirth = dateOfBirth;
IsActive = true;
}
// Domain methods with validation
public void SetName(string firstName, string lastName)
{
FirstName = Check.NotNullOrWhiteSpace(firstName, nameof(firstName), maxLength: 100);
LastName = Check.NotNullOrWhiteSpace(lastName, nameof(lastName), maxLength: 100);
}
public void SetEmail(string email)
{
Email = Check.NotNullOrWhiteSpace(email, nameof(email), maxLength: 256);
}
public void Activate() => IsActive = true;
public void Deactivate() => IsActive = false;
}
論理削除
public class Patient : FullAuditedAggregateRoot<Guid>, ISoftDelete
{
public bool IsDeleted { get; set; }
// ABP automatically filters out soft-deleted entities
}
マルチテナント
public class Patient : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
// ABP automatically filters by current tenant
}
監査フィールド
FullAuditedAggregateRoot<Guid> は次のフィールドを提供します:
CreationTime、CreatorIdLastModificationTime、LastModifierIdIsDeleted、DeletionTime、DeleterId
リポジトリパターン
ジェネリックリポジトリの使用
public class PatientAppService : ApplicationService
{
private readonly IRepository<Patient, Guid> _patientRepository;
public PatientAppService(IRepository<Patient, Guid> patientRepository)
{
_patientRepository = patientRepository;
}
public async Task<PatientDto> GetAsync(Guid id)
{
var patient = await _patientRepository.GetAsync(id);
return ObjectMapper.Map<Patient, PatientDto>(patient);
}
public async Task<PagedResultDto<PatientDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var totalCount = await _patientRepository.GetCountAsync();
var queryable = await _patientRepository.GetQueryableAsync();
var patients = await AsyncExecuter.ToListAsync(
queryable
.OrderBy(input.Sorting ?? nameof(Patient.FirstName))
.PageBy(input.SkipCount, input.MaxResultCount));
return new PagedResultDto<PatientDto>(
totalCount,
ObjectMapper.Map<List<Patient>, List<PatientDto>>(patients));
}
}
カスタムリポジトリ
Domain レイヤーでインターフェイスを定義します:
public interface IPatientRepository : IRepository<Patient, Guid>
{
Task<List<Patient>> GetActivePatientsByDoctorAsync(Guid doctorId);
Task<Patient?> FindByEmailAsync(string email);
}
EntityFrameworkCore レイヤーで実装します:
public class PatientRepository : EfCoreRepository<ClinicDbContext, Patient, Guid>, IPatientRepository
{
public PatientRepository(IDbContextProvider<ClinicDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<List<Patient>> GetActivePatientsByDoctorAsync(Guid doctorId)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.Where(p => p.PrimaryDoctorId == doctorId && p.IsActive)
.Include(p => p.Appointments)
.ToListAsync();
}
public async Task<Patient?> FindByEmailAsync(string email)
{
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(p => p.Email == email);
}
}
ドメインサービス
ビジネスロジックが複数のエンティティまたは外部のドメイン概念に関わる場合は、ドメインサービスを使用します。
public class AppointmentManager : DomainService
{
private readonly IRepository<Appointment, Guid> _appointmentRepository;
private readonly IRepository<DoctorSchedule, Guid> _scheduleRepository;
public AppointmentManager(
IRepository<Appointment, Guid> appointmentRepository,
IRepository<DoctorSchedule, Guid> scheduleRepository)
{
_appointmentRepository = appointmentRepository;
_scheduleRepository = scheduleRepository;
}
public async Task<Appointment> CreateAsync(
Guid patientId,
Guid doctorId,
DateTime appointmentDate,
string description)
{
// Business rule: Check if doctor is available
await CheckDoctorAvailabilityAsync(doctorId, appointmentDate);
// Business rule: Check for conflicts
await CheckAppointmentConflictsAsync(doctorId, appointmentDate);
var appointment = new Appointment(
GuidGenerator.Create(),
patientId,
doctorId,
appointmentDate,
description);
return await _appointmentRepository.InsertAsync(appointment);
}
private async Task CheckDoctorAvailabilityAsync(Guid doctorId, DateTime appointmentDate)
{
var schedule = await _scheduleRepository.FirstOrDefaultAsync(
s => s.DoctorId == doctorId && s.DayOfWeek == appointmentDate.DayOfWeek);
if (schedule == null)
throw new BusinessException("Doctor not available on this day");
var timeOfDay = appointmentDate.TimeOfDay;
if (timeOfDay < schedule.StartTime || timeOfDay > schedule.EndTime)
throw new BusinessException("Doctor not available at this time");
}
private async Task CheckAppointmentConflictsAsync(Guid doctorId, DateTime appointmentDate)
{
var hasConflict = await _appointmentRepository.AnyAsync(a =>
a.DoctorId == doctorId &&
a.AppointmentDate == appointmentDate &&
a.Status != AppointmentStatus.Cancelled);
if (hasConflict)
throw new BusinessException("Doctor already has an appointment at this time");
}
}
データシード
IDataSeedContributor パターン
public class ClinicDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Doctor, Guid> _doctorRepository;
private readonly IGuidGenerator _guidGenerator;
public ClinicDataSeedContributor(
IRepository<Doctor, Guid> doctorRepository,
IGuidGenerator guidGenerator)
{
_doctorRepository = doctorRepository;
_guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
// Idempotent check
if (await _doctorRepository.GetCountAsync() > 0)
return;
var doctors = new List<Doctor>
{
new Doctor(_guidGenerator.Create(), "Dr. Smith", "Cardiology", "[email protected]"),
new Doctor(_guidGenerator.Create(), "Dr. Jones", "Pediatrics", "[email protected]"),
};
foreach (var doctor in doctors)
{
await _doctorRepository.InsertAsync(doctor);
}
}
}
テストデータのシード
public class ClinicTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
public static readonly Guid TestPatientId = Guid.Parse("2e701e62-0953-4dd3-910b-dc6cc93ccb0d");
public static readonly Guid TestDoctorId = Guid.Parse("3a801f73-1064-5ee4-a21c-ed7dd4ddc1e");
public async Task SeedAsync(DataSeedContext context)
{
await _patientRepository.InsertAsync(new Patient(
TestPatientId, "Test", "Patient", "[email protected]", DateTime.Now.AddYears(-30)));
await _doctorRepository.InsertAsync(new Doctor(
TestDoctorId, "Test Doctor", "General", "[email protected]"));
}
}
ベストプラクティス
- 状態をカプセル化する - プライベートセッターとドメインメソッドを使用します
- コンストラクターで検証する - エンティティが常に有効であることを保証します
- 値オブジェクトを使用する - 複雑なプロパティ(Address、Money)に使用します
- エンティティにドメインロジックを記述する - シンプルなルールはエンティティに記述します
- ドメインサービス - エンティティ間のロジックに使用します
- カスタムリポジトリ - カスタムクエリが必要な場合にのみ使用します
- 冪等なシード処理 - 挿入前に必ず確認します
関連スキル
abp-service-patterns- アプリケーションレイヤーのパターンabp-infrastructure-patterns- 横断的関心事efcore-patterns- データベース構成
コメント
Markdown 対応