messaging について
messaging は、.NET のメッセージングに関する指針を示すスキルです。Wolverine を標準案として推奨し、代替として MassTransit を紹介しながら、信頼性の高い配信、ハンドラーパターン、コレオグラフィーと Saga の選択を扱います。
主な機能
- 01Wolverine の設定とハンドラー検出
- 02イベントの発行・処理パターン
- 03トランザクショナル Outbox の指針
- 04ステートフルなフロー向け Saga オーケストレーション
- 05Wolverine と MassTransit の比較
- 06メッセージ契約の設計指針
活用シーン
- 01トランザクショナル Outbox によるイベントの確実な発行
- 02メッセージを介したアプリケーションモジュールの連携
- 03コレオグラフィーや Saga による複数段階ワークフローのモデル化
- 04.NET メッセージングライブラリの選定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill messagingインストール後、Claude Code で/messagingを入力して読み込まれたか確認します。
詳細
メッセージング
基本原則
- Wolverine を推奨されるデフォルトとして使います — MIT ライセンスで、Mediator とメッセージングを 1 つのライブラリに統合し、組み込みのアウトボックス、Saga のサポート、規約ベースのハンドラーを備えています。MassTransit も代替手段ですが、v9 から商用ライセンスが必要です。
- 信頼性のためにアウトボックスパターンを使います — データベーストランザクションが成功した場合にのみメッセージが発行されるよう、必ずトランザクショナルアウトボックスを使います。
- シンプルなフローには振る舞いの連鎖、複雑なフローには Saga を使います — ワークフローが 2~3 ステップの場合は、イベントの振る舞いの連鎖を使います。補償アクションや複雑な状態がある場合は、Saga を使います。
- メッセージはコントラクトです — メッセージ型は共有コントラクトプロジェクトに配置します。プリミティブ型を使ったシンプルなレコードにします。
パターン
Wolverine のセットアップ
// Program.cs
builder.Host.UseWolverine(opts =>
{
// Auto-discover handlers from this assembly
opts.Discovery.IncludeAssembly(typeof(Program).Assembly);
// RabbitMQ transport
opts.UseRabbitMq(rabbit =>
{
rabbit.HostName = "localhost";
// Or from configuration:
// rabbit.HostName = builder.Configuration["RabbitMq:Host"]!;
})
.AutoProvision() // Create queues/exchanges automatically
.AutoPurgeOnStartup(); // Dev only — clear queues on startup
// Enable transactional outbox with EF Core
opts.Services.AddDbContextWithWolverineIntegration<AppDbContext>(x =>
x.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
opts.Policies.AutoApplyTransactions(); // Wrap handlers in DB transactions
});
理由:UseWolverine() は、ハンドラーの検出、トランスポート、アウトボックスを 1 か所で登録します。AutoProvision() により、開発中のブローカーの手動セットアップが不要になります。
イベントの発行
Wolverine は、カスケードメッセージ(戻り値)と明示的な発行という 2 つの発行方法をサポートしています。
// Message contract (in shared Contracts project)
public record OrderCreated(Guid OrderId, string CustomerId, decimal Total, DateTimeOffset CreatedAt);
// Style 1: Cascading messages — return the event from the handler
// Wolverine automatically publishes returned messages after the handler completes.
public static class CreateOrder
{
public record Command(string CustomerId, List<OrderItem> Items);
public record Response(Guid OrderId, decimal Total);
public static async Task<(Response, OrderCreated)> HandleAsync(
Command command, AppDbContext db, TimeProvider clock, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
var response = new Response(order.Id, order.Total);
var @event = new OrderCreated(order.Id, order.CustomerId, order.Total, order.CreatedAt);
return (response, @event); // Both are published automatically
}
}
// Style 2: Explicit publishing via IMessageBus
public static class CreateOrder
{
public record Command(string CustomerId, List<OrderItem> Items);
public record Response(Guid OrderId, decimal Total);
public static async Task<Response> HandleAsync(
Command command, AppDbContext db, IMessageBus bus, TimeProvider clock, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
await bus.PublishAsync(new OrderCreated(
order.Id, order.CustomerId, order.Total, order.CreatedAt));
return new Response(order.Id, order.Total);
}
}
理由:カスケードメッセージ(タプルの戻り値)はシンプルでテストしやすく、ハンドラーを純粋関数にできます。発行が条件付きの場合や、複数のイベントが必要な場合は、明示的な IMessageBus を使います。
イベントの受信
Wolverine は規約ベースのハンドラーを使用するため、インターフェースも基底クラスも必要ありません。メッセージ型を最初のパラメーターに持つ Handle メソッドを用意するだけです。
// Notifications module — handles OrderCreated from Orders module
public static class OrderCreatedHandler
{
public static async Task HandleAsync(
OrderCreated message, NotificationsDbContext db, ILogger logger, CancellationToken ct)
{
logger.LogInformation("Processing OrderCreated: {OrderId}", message.OrderId);
var notification = new OrderNotification(message.OrderId, message.CustomerId);
db.Notifications.Add(notification);
await db.SaveChangesAsync(ct);
}
}
理由:規約ベースのハンドラーには定型的なコードが不要です。Wolverine はシグネチャーに基づいてハンドラーを検出します。メッセージ型を最初のパラメーターに持つ、Handle/HandleAsync/Consume/ConsumeAsync という名前のパブリックメソッドが対象です。
トランザクショナルアウトボックス
データベーストランザクションが成功した場合にのみ、メッセージが発行されるようにします。
// 1. Register DbContext with Wolverine integration
builder.Host.UseWolverine(opts =>
{
opts.Services.AddDbContextWithWolverineIntegration<AppDbContext>(x =>
x.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
opts.Policies.AutoApplyTransactions();
});
// 2. DbContext — add Wolverine outbox tables
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Wolverine inbox/outbox tables — required for transactional messaging
modelBuilder.AddIncomingWolverineMessageTable();
modelBuilder.AddOutgoingWolverineMessageTable();
}
}
理由:AddDbContextWithWolverineIntegration と AutoApplyTransactions は、アウトボックスへの書き込みを含むトランザクションで各ハンドラーを実行します。メッセージはトランザクションのコミット後にのみ送信されるため、二重書き込みの問題は発生しません。
Saga(状態を持つオーケストレーション)
Wolverine の Saga は、Start メソッドと Handle メソッドを持つ Saga<T> 基底クラスを使用します。カスケードメッセージによって Saga が進行します。
public record OrderSagaState(Guid Id)
{
public string? CustomerId { get; set; }
public bool PaymentReceived { get; set; }
}
public class OrderSaga : Saga<OrderSagaState>
{
public Guid Id { get; set; }
// Start the saga when an OrderCreated event arrives
public static (OrderSagaState, ProcessPayment) Start(OrderCreated message)
{
var state = new OrderSagaState(message.OrderId)
{
CustomerId = message.CustomerId
};
var command = new ProcessPayment(message.OrderId, message.Total);
return (state, command); // State is persisted, command is sent
}
// Handle payment result
public CompleteOrder Handle(PaymentCompleted message)
{
PaymentReceived = true;
MarkCompleted(); // Ends the saga
return new CompleteOrder(Id);
}
// Compensating action on failure
public CancelOrder Handle(PaymentFailed message)
{
MarkCompleted();
return new CancelOrder(Id);
}
}
理由:Wolverine の Saga は、ステートマシン DSL ではなくシンプルな C# メソッドを使用します。各ハンドラーは、ワークフローを進行させるカスケードメッセージを返します。MarkCompleted() は Saga の状態をクリーンアップします。
代替手段:MassTransit
MassTransit は成熟した代替手段ですが、v9 以降は商用ライセンスが必要です。主な API の概要は次のとおりです。
// Setup
builder.Services.AddMassTransit(x =>
{
x.SetKebabCaseEndpointNameFormatter();
x.AddConsumers(typeof(Program).Assembly);
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(builder.Configuration.GetConnectionString("RabbitMq"));
cfg.ConfigureEndpoints(context);
});
});
// Publishing
await publishEndpoint.Publish(new OrderCreated(...), ct);
// Consuming — requires IConsumer<T> interface
public class OrderCreatedConsumer(AppDbContext db) : IConsumer<OrderCreated>
{
public async Task Consume(ConsumeContext<OrderCreated> context)
{
var message = context.Message;
// Handle event...
}
}
// Outbox
x.AddEntityFrameworkOutbox<AppDbContext>(o =>
{
o.UsePostgres();
o.UseBusOutbox();
});
// Saga — uses MassTransitStateMachine<TState>
public class OrderSaga : MassTransitStateMachine<OrderSagaState> { /* ... */ }
ライセンスに関する注意:MassTransit v9 以降を本番環境で使用するには、商用ライセンスが必要です。新規プロジェクトでは、Wolverine(MIT)を推奨されるデフォルトとして使います。
アンチパターン
アウトボックスなしでイベントを発行しない
// BAD — if SaveChanges succeeds but Publish fails, data is inconsistent
await db.SaveChangesAsync(ct);
await bus.PublishAsync(new OrderCreated(...));
// GOOD — use transactional outbox (messages are in the same transaction)
// Configure AddDbContextWithWolverineIntegration() + AutoApplyTransactions()
// Wolverine handles this automatically
メッセージコントラクトに複雑なロジックを含めない
// BAD — behavior in a message
public record OrderCreated(Guid OrderId)
{
public decimal CalculateShipping() => /* logic */; // DON'T
}
// GOOD — messages are pure data
public record OrderCreated(Guid OrderId, string CustomerId, decimal Total, DateTimeOffset CreatedAt);
重要なイベントに Fire-and-Forget を使わない
// BAD — no guarantee of delivery
_ = Task.Run(() => bus.PublishAsync(new OrderCreated(...)));
// GOOD — await the publish (with outbox, this is transactional)
await bus.PublishAsync(new OrderCreated(...));
選択ガイド
| シナリオ | 推奨事項 |
|---|---|
| モジュール間通信(新規プロジェクト) | イベントを使う Wolverine(MIT、無料) |
| モジュール間通信(既存の MassTransit) | MassTransit(v9 から商用ライセンスが必要) |
| 信頼性の高いイベント発行 | トランザクショナルアウトボックス(Wolverine と MassTransit の両方がサポート) |
| シンプルな 2~3 ステップのワークフロー | イベントの振る舞いの連鎖 |
| 補償処理を伴う複雑なワークフロー | Wolverine Saga または MassTransit Saga |
| ローカル開発用ブローカー | RabbitMQ(Docker または Aspire 経由) |
| 本番環境のクラウドブローカー | Azure Service Bus または RabbitMQ |
| Mediator とメッセージングを単一のライブラリで使いたい場合 | Wolverine(Mediator と MassTransit の両方を置き換え) |