Skip to main content

add-integration-event

Publish a cross-module integration event via the Outbox and handle it idempotently in another module. Use when one module must react to something that happened in another. See .agents/rules/eventing.md.

소스 정보

저장소
fullstackhero/dotnet-starter-kit
최근 소스 활동
2026년 8월 7일 08:13
감지된 SKILL.md 언어
영어
스타
6,813
포크
2,003

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
add-integration-event
description
Publish a cross-module integration event via the Outbox and handle it idempotently in another module. Use when one module must react to something that happened in another. See .agents/rules/eventing.md.
argument-hint
[SourceModule] [EventName] [ConsumerModule]
# Add Integration Event Cross-module communication goes through **integration events + the Outbox** (transactional, crash-safe) — never a direct in-process call into another module's runtime, and never `IEventBus.PublishAsync` from a handler. Full model: `.agents/rules/eventing.md`. ## Step 1 — Define the event (source module's Contracts) `Modules.{Source}.Contracts/Events/{Event}IntegrationEvent.cs` — implement `IIntegrationEvent`: ```csharp public sealed record {Event}IntegrationEvent( Guid Id, DateTime OccurredOnUtc, string? TenantId, string CorrelationId, string Source, Guid {Entity}Id, string SomePayload) : IIntegrationEvent; ``` ⚠️ Don't rename/move this type later — the outbox stores its assembly-qualified name; a rename makes `Type.GetType()` return null and the message dead-letters. Keep the type name + namespace stable. ## Step 2 — Publish via the Outbox (source handler) Publishing needs **no module registration** — the outbox is framework-owned and the host wires it once. Inject `IOutboxWriter` (from `FSH.Framework.Eventing.Abstractions`) and add the event in the same unit of work: ```csharp public sealed class Do{Thing}CommandHandler({Source}DbContext db, IOutboxWriter outbox) : ICommandHandler<Do{Thing}Command, Unit> { public async ValueTask<Unit> Handle(Do{Thing}Command command, CancellationToken cancellationToken) { // … mutate entities, db.SaveChangesAsync … var evt = new {Event}IntegrationEvent( Id: Guid.CreateVersion7(), OccurredOnUtc: DateTime.UtcNow, TenantId: /* current tenant */, CorrelationId: Guid.NewGuid().ToString(), Source: "{Source}", {Entity}Id: entity.Id, SomePayload: "…"); await outbox.AddAsync(evt, cancellationToken).ConfigureAwait(false); return Unit.Value; } } ``` The `OutboxDispatcherHostedService` later publishes it via `IEventBus`. ## Step 3 — Handle it (consumer module) `Modules.{Consumer}/IntegrationEventHandlers/{Event}IntegrationEventHandler.cs` — `sealed`, implement `IIntegrationEventHandler<T>`: ```csharp public sealed class {Event}IntegrationEventHandler({Consumer}DbContext db /*, IHubContext<AppHub> hub */) : IIntegrationEventHandler<{Event}IntegrationEvent> { public async Task HandleAsync({Event}IntegrationEvent @event, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(@event); // … write to the consumer's tables / push a notification … await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } } ``` Register the consumer's handlers in its `ConfigureServices`: ```csharp builder.Services.AddIntegrationEventHandlers(typeof({Consumer}Module).Assembly); ``` ## Gotchas - **Idempotency is free** with the in-memory bus (the Inbox dedups by `{eventId, handlerName}`) — don't hand-roll it. - The in-memory bus runs handlers **synchronously in the publisher's scope** — keep the handler lean; a throw surfaces to the originating request. Published via the outbox, that scope belongs to the dispatcher, so the consumer runs on the next cycle and its failures never reach the caller. Don't let a caller (or a test) assume the side effect already happened; integration tests drain with `OutboxDrain.DrainAsync`. - If the handler reads a **tenant-filtered** DbContext from a background path (open-generic handler, Hangfire job), restore Finbuckle context first via `IMultiTenantContextSetter` (see `WebhookFanoutHandler`). - **Module load order:** the consumer must load before the publisher if it must react (`Order` in `[assembly: FshModule]`) — e.g. Notifications (750) before Chat (800). ## Checklist - [ ] Event in source Contracts, implements `IIntegrationEvent`, stable type name - [ ] Published via `IOutboxWriter.AddAsync` (not the bus); no per-module eventing registration needed - [ ] Consumer handler `sealed : IIntegrationEventHandler<T>`; `AddIntegrationEventHandlers(assembly)` registered - [ ] Background readers restore tenant context; module `Order` lets the consumer load first - [ ] Build + tests green
GitHub에서 보기