abp-infrastructure-patterns について
ABP Frameworkを使う開発者向けに、インフラストラクチャのパターンをC#例で示すリファレンスです。権限、バックグラウンドジョブ、分散イベント、マルチテナント、モジュール設定を扱います。
主な機能
- 01権限の定義、登録、利用
- 02バックグラウンドジョブの作成と予約
- 03分散イベントの発行と処理
- 04テナント横断操作とデータ投入
- 05モジュール依存関係とオブジェクト拡張の設定
活用シーン
- 01アプリケーションサービスへの権限確認の追加
- 02遅延ジョブや長時間ジョブの予約
- 03モジュール間でのドメインイベント連携
- 04マルチテナントのデータ操作の実装
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ thapaliyabikendra/ ai-artifacts --skill abp-infrastructure-patternsインストール後、Claude Code で/abp-infrastructure-patternsを入力して読み込まれたか確認します。
詳細
ABP インフラストラクチャパターン
ABP Framework の横断的関心事とインフラストラクチャパターンです。
認可と権限
権限の定義
// Domain.Shared/Permissions/ClinicPermissions.cs
public static class ClinicPermissions
{
public const string GroupName = "Clinic";
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 static class Appointments
{
public const string Default = GroupName + ".Appointments";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
public const string ViewAll = Default + ".ViewAll"; // Admins only
}
}
権限の登録
// Application.Contracts/Permissions/ClinicPermissionDefinitionProvider.cs
public class ClinicPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var clinicGroup = context.AddGroup(ClinicPermissions.GroupName);
var patients = clinicGroup.AddPermission(
ClinicPermissions.Patients.Default,
L("Permission:Patients"));
patients.AddChild(ClinicPermissions.Patients.Create, L("Permission:Patients.Create"));
patients.AddChild(ClinicPermissions.Patients.Edit, L("Permission:Patients.Edit"));
patients.AddChild(ClinicPermissions.Patients.Delete, L("Permission:Patients.Delete"));
var appointments = clinicGroup.AddPermission(
ClinicPermissions.Appointments.Default,
L("Permission:Appointments"));
appointments.AddChild(ClinicPermissions.Appointments.Create, L("Permission:Appointments.Create"));
appointments.AddChild(ClinicPermissions.Appointments.ViewAll, L("Permission:Appointments.ViewAll"));
}
private static LocalizableString L(string name)
=> LocalizableString.Create<ClinicResource>(name);
}
権限の使用
// Declarative
[Authorize(ClinicPermissions.Patients.Create)]
public async Task<PatientDto> CreateAsync(CreatePatientDto input) { }
// Imperative
public async Task<AppointmentDto> GetAsync(Guid id)
{
var appointment = await _appointmentRepository.GetAsync(id);
if (appointment.DoctorId != CurrentUser.Id)
{
await AuthorizationService.CheckAsync(ClinicPermissions.Appointments.ViewAll);
}
return _mapper.AppointmentToDto(appointment);
}
// Check without throwing
public async Task<bool> CanCreatePatientAsync()
=> await AuthorizationService.IsGrantedAsync(ClinicPermissions.Patients.Create);
バックグラウンドジョブ
ジョブの定義
public class AppointmentReminderJob : AsyncBackgroundJob<AppointmentReminderArgs>, ITransientDependency
{
private readonly IRepository<Appointment, Guid> _appointmentRepository;
private readonly IEmailSender _emailSender;
public AppointmentReminderJob(
IRepository<Appointment, Guid> appointmentRepository,
IEmailSender emailSender)
{
_appointmentRepository = appointmentRepository;
_emailSender = emailSender;
}
public override async Task ExecuteAsync(AppointmentReminderArgs args)
{
var appointment = await _appointmentRepository.GetAsync(args.AppointmentId);
await _emailSender.SendAsync(
appointment.Patient.Email,
"Appointment Reminder",
$"You have an appointment on {appointment.AppointmentDate}");
}
}
public class AppointmentReminderArgs
{
public Guid AppointmentId { get; set; }
}
ジョブのキューへの登録
public async Task<AppointmentDto> CreateAsync(CreateAppointmentDto input)
{
var appointment = await _appointmentManager.CreateAsync(/*...*/);
// Schedule reminder 24 hours before
var reminderTime = appointment.AppointmentDate.AddHours(-24);
await _backgroundJobManager.EnqueueAsync(
new AppointmentReminderArgs { AppointmentId = appointment.Id },
delay: reminderTime - DateTime.Now);
return _mapper.AppointmentToDto(appointment);
}
分散イベント
イベントの発行
// From entity (recommended for domain events)
public class Patient : AggregateRoot<Guid>
{
public void Activate()
{
IsActive = true;
AddDistributedEvent(new PatientActivatedEto
{
Id = Id,
Name = Name,
Email = Email
});
}
}
// From application service
public async Task ActivateAsync(Guid id)
{
var patient = await _patientRepository.GetAsync(id);
patient.Activate();
// Or manually publish:
await _distributedEventBus.PublishAsync(new PatientActivatedEto
{
Id = patient.Id,
Name = patient.Name,
Email = patient.Email
});
}
イベントの処理
public class PatientActivatedEventHandler :
IDistributedEventHandler<PatientActivatedEto>,
ITransientDependency
{
private readonly IEmailSender _emailSender;
private readonly ILogger<PatientActivatedEventHandler> _logger;
public PatientActivatedEventHandler(
IEmailSender emailSender,
ILogger<PatientActivatedEventHandler> logger)
{
_emailSender = emailSender;
_logger = logger;
}
public async Task HandleEventAsync(PatientActivatedEto eventData)
{
_logger.LogInformation("Patient activated: {Name}", eventData.Name);
await _emailSender.SendAsync(
eventData.Email,
"Welcome",
"Your patient account has been activated");
}
}
堅牢なイベントハンドラー(冪等性+マルチテナント対応)
public class EntitySyncEventHandler :
IDistributedEventHandler<EntityUpdatedEto>,
ITransientDependency
{
private readonly IRepository<Entity, Guid> _repository;
private readonly IDataFilter _dataFilter;
private readonly ILogger<EntitySyncEventHandler> _logger;
public async Task HandleEventAsync(EntityUpdatedEto eto)
{
// Disable tenant filter for cross-tenant sync
using (_dataFilter.Disable<IMultiTenant>())
{
try
{
_logger.LogInformation("Processing entity sync: {Id}", eto.Id);
// Idempotency check
var existing = await _repository.FirstOrDefaultAsync(
x => x.ExternalId == eto.ExternalId);
if (existing != null)
{
ObjectMapper.Map(eto, existing);
await _repository.UpdateAsync(existing);
}
else
{
var entity = ObjectMapper.Map<EntityUpdatedEto, Entity>(eto);
await _repository.InsertAsync(entity);
}
_logger.LogInformation("Entity sync completed: {Id}", eto.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Entity sync failed: {Id}", eto.Id);
throw new UserFriendlyException($"Failed to sync entity: {ex.Message}");
}
}
}
}
マルチテナント
テナントをまたぐ操作
public class CrossTenantService : ApplicationService
{
private readonly IDataFilter _dataFilter;
private readonly ICurrentTenant _currentTenant;
public async Task<List<PatientDto>> GetAllTenantsPatients()
{
using (_dataFilter.Disable<IMultiTenant>())
{
return await _patientRepository.GetListAsync();
}
}
public async Task OperateOnTenant(Guid tenantId)
{
using (_currentTenant.Change(tenantId))
{
await DoTenantSpecificOperation();
}
}
}
テナント固有のデータシード
public async Task SeedAsync(DataSeedContext context)
{
if (context.TenantId.HasValue)
await SeedTenantDataAsync(context.TenantId.Value);
else
await SeedHostDataAsync();
}
モジュールの構成
[DependsOn(
typeof(ClinicDomainModule),
typeof(AbpIdentityDomainModule),
typeof(AbpPermissionManagementDomainModule))]
public class ClinicApplicationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpIdentityOptions>(options =>
{
options.ExternalLoginProviders.Add<GoogleExternalLoginProvider>();
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "Clinic:";
});
context.Services.AddTransient<IPatientAppService, PatientAppService>();
context.Services.AddSingleton<ClinicApplicationMappers>();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
}
}
オブジェクトの拡張
public static class ClinicModuleExtensionConfigurator
{
public static void Configure()
{
ObjectExtensionManager.Instance.Modules()
.ConfigureIdentity(identity =>
{
identity.ConfigureUser(user =>
{
user.AddOrUpdateProperty<string>(
"Title",
property =>
{
property.Attributes.Add(new StringLengthAttribute(64));
});
});
});
}
}
ベストプラクティス
- 権限 - 階層的に定義します(Parent.Child パターン)。
- バックグラウンドジョブ - 長時間かかるタスクや遅延タスクに使用します。
- 分散イベント - モジュール間の疎結合に使用します。
- 冪等性 - イベントハンドラーでは、挿入前に既存データがないか確認します。
- マルチテナント -
IDataFilter.Disable<IMultiTenant>()は必要最小限に使用します。 - モジュールの依存関係 - すべての依存関係を明示的に宣言します。
関連するスキル
abp-entity-patterns- ドメイン層のパターンabp-service-patterns- アプリケーション層のパターンopeniddict-authorization- OAuth の実装distributed-events-advanced- 高度なイベントパターン
コメント
Markdown 対応