messaging

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Messaging

消息传递

Core Principles

核心原则

  1. Wolverine is the recommended default — MIT licensed, combines mediator + messaging in one library with built-in outbox, saga support, and convention-based handlers. MassTransit is an alternative but requires a commercial license from v9.
  2. Outbox pattern for reliability — Always use the transactional outbox to ensure messages are published only when the database transaction succeeds.
  3. Choreography for simple flows, saga for complex — If a workflow has 2-3 steps, use event choreography. If it has compensating actions or complex state, use a saga.
  4. Messages are contracts — Put message types in a shared contracts project. Keep them as simple records with primitive types.
  1. 推荐默认使用Wolverine — MIT协议授权,将中介者模式+消息传递整合到一个库中,内置事务性发件箱、Saga支持和基于约定的处理器。MassTransit是替代方案,但从v9版本开始需要商业许可证。
  2. 使用事务性发件箱保障可靠性 — 始终使用事务性发件箱,确保仅当数据库事务成功时才发布消息。
  3. 简单流程用编排,复杂流程用Saga — 如果工作流有2-3个步骤,使用事件编排。如果存在补偿操作或复杂状态,使用Saga。
  4. 消息是契约 — 将消息类型放在共享契约项目中。保持为包含原始类型的简单记录。

Patterns

模式

Wolverine Setup

Wolverine 配置

csharp
// 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
});
Why:
UseWolverine()
registers handler discovery, transport, and outbox in one place.
AutoProvision()
eliminates manual broker setup during development.
csharp
// Program.cs
builder.Host.UseWolverine(opts =>
{
    // 自动发现当前程序集中的处理器
    opts.Discovery.IncludeAssembly(typeof(Program).Assembly);

    // RabbitMQ 传输
    opts.UseRabbitMq(rabbit =>
    {
        rabbit.HostName = "localhost";
        // 或从配置读取:
        // rabbit.HostName = builder.Configuration["RabbitMq:Host"]!;
    })
    .AutoProvision()   // 自动创建队列/交换器
    .AutoPurgeOnStartup(); // 仅开发环境使用——启动时清空队列

    // 启用EF Core集成的事务性发件箱
    opts.Services.AddDbContextWithWolverineIntegration<AppDbContext>(x =>
        x.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

    opts.Policies.AutoApplyTransactions(); // 将处理器包装在数据库事务中
});
原因:
UseWolverine()
一站式注册处理器发现、传输和发件箱功能。
AutoProvision()
消除了开发期间手动配置代理的工作。

Publishing Events

发布事件

Wolverine supports two publishing styles: cascading messages (return values) and explicit publishing.
csharp
// 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
    }
}
csharp
// 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);
    }
}
Why: Cascading messages (tuple return) are simpler and testable — the handler is a pure function. Use explicit
IMessageBus
when publishing is conditional or requires multiple events.
Wolverine支持两种发布方式:级联消息(返回值)和显式发布。
csharp
// 消息契约(位于共享Contracts项目中)
public record OrderCreated(Guid OrderId, string CustomerId, decimal Total, DateTimeOffset CreatedAt);

// 方式1:级联消息——从处理器返回事件
// Wolverine会在处理器完成后自动发布返回的消息。
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); // 两者都会自动发布
    }
}
csharp
// 方式2:通过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

Consuming Events

消费事件

Wolverine uses convention-based handlers — no interface, no base class. Just a
Handle
method with the message type as the first parameter.
csharp
// 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);
    }
}
Why: Convention-based handlers have zero ceremony. Wolverine discovers them by signature: any public method named
Handle
/
HandleAsync
/
Consume
/
ConsumeAsync
with the message type as the first parameter.
Wolverine使用基于约定的处理器——无需接口或基类。只需一个以消息类型为第一个参数的
Handle
方法。
csharp
// 通知模块——处理来自订单模块的OrderCreated事件
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
的公共方法,且第一个参数为消息类型。

Transactional Outbox

事务性发件箱

Ensures messages are only published if the database transaction succeeds.
csharp
// 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();
    }
}
Why:
AddDbContextWithWolverineIntegration
+
AutoApplyTransactions
wraps every handler in a transaction that includes outbox writes. Messages are only sent after the transaction commits — no dual-write problem.
确保仅当数据库事务成功时才发布消息。
csharp
// 1. 注册与Wolverine集成的DbContext
builder.Host.UseWolverine(opts =>
{
    opts.Services.AddDbContextWithWolverineIntegration<AppDbContext>(x =>
        x.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

    opts.Policies.AutoApplyTransactions();
});

// 2. DbContext — 添加Wolverine发件箱表
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Wolverine收件箱/发件箱表——事务性消息传递必需
        modelBuilder.AddIncomingWolverineMessageTable();
        modelBuilder.AddOutgoingWolverineMessageTable();
    }
}
原因:
AddDbContextWithWolverineIntegration
+
AutoApplyTransactions
将每个处理器包装在包含发件箱写入操作的事务中。仅在事务提交后才发送消息——避免双写问题。

Saga (Stateful Orchestration)

Saga(有状态编排)

Wolverine sagas use a
Saga<T>
base class with
Start
and
Handle
methods. Cascading messages drive the saga forward.
csharp
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);
    }
}
Why: Wolverine sagas use simple C# methods instead of a state machine DSL. Each handler returns cascading messages to drive the workflow.
MarkCompleted()
cleans up the saga state.
Wolverine的Saga使用
Saga<T>
基类,包含
Start
Handle
方法。级联消息驱动Saga推进。
csharp
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; }

    // 当OrderCreated事件到达时启动Saga
    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); // 状态被持久化,命令被发送
    }

    // 处理支付结果
    public CompleteOrder Handle(PaymentCompleted message)
    {
        PaymentReceived = true;
        MarkCompleted(); // 结束Saga
        return new CompleteOrder(Id);
    }

    // 失败时的补偿操作
    public CancelOrder Handle(PaymentFailed message)
    {
        MarkCompleted();
        return new CancelOrder(Id);
    }
}
原因: Wolverine的Saga使用简单的C#方法,而非状态机DSL。每个处理器返回级联消息以驱动工作流。
MarkCompleted()
会清理Saga状态。

Alternative: MassTransit

替代方案:MassTransit

MassTransit is a mature alternative with a commercial license requirement from v9+. Key API surface:
csharp
// 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> { /* ... */ }
License note: MassTransit v9+ requires a commercial license for production use. Wolverine (MIT) is the recommended default for new projects.
MassTransit是成熟的替代方案,从v9+版本开始需要商业许可证。核心API如下:
csharp
// 配置
builder.Services.AddMassTransit(x =>
{
    x.SetKebabCaseEndpointNameFormatter();
    x.AddConsumers(typeof(Program).Assembly);
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host(builder.Configuration.GetConnectionString("RabbitMq"));
        cfg.ConfigureEndpoints(context);
    });
});

// 发布
await publishEndpoint.Publish(new OrderCreated(...), ct);

// 消费——需要IConsumer<T>接口
public class OrderCreatedConsumer(AppDbContext db) : IConsumer<OrderCreated>
{
    public async Task Consume(ConsumeContext<OrderCreated> context)
    {
        var message = context.Message;
        // 处理事件...
    }
}

// 发件箱
x.AddEntityFrameworkOutbox<AppDbContext>(o =>
{
    o.UsePostgres();
    o.UseBusOutbox();
});

// Saga — 使用MassTransitStateMachine<TState>
public class OrderSaga : MassTransitStateMachine<OrderSagaState> { /* ... */ }
许可证说明: MassTransit v9+版本在生产环境使用需要商业许可证。新项目推荐使用Wolverine(MIT协议,免费)。

Anti-patterns

反模式

Don't Publish Events Without Outbox

不要不使用发件箱就发布事件

csharp
// 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
csharp
// 错误示例——如果SaveChanges成功但Publish失败,数据会不一致
await db.SaveChangesAsync(ct);
await bus.PublishAsync(new OrderCreated(...));

// 正确示例——使用事务性发件箱(消息在同一事务中)
// 配置AddDbContextWithWolverineIntegration() + AutoApplyTransactions()
// Wolverine会自动处理

Don't Put Complex Logic in Message Contracts

不要在消息契约中放入复杂逻辑

csharp
// 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);
csharp
// 错误示例——消息中包含行为
public record OrderCreated(Guid OrderId)
{
    public decimal CalculateShipping() => /* 逻辑 */; // 不要这样做
}

// 正确示例——消息仅包含纯数据
public record OrderCreated(Guid OrderId, string CustomerId, decimal Total, DateTimeOffset CreatedAt);

Don't Use Fire-and-Forget for Important Events

重要事件不要使用即发即弃模式

csharp
// 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(...));
csharp
// 错误示例——无法保证送达
_ = Task.Run(() => bus.PublishAsync(new OrderCreated(...)));

// 正确示例——等待发布完成(结合发件箱,这是事务性的)
await bus.PublishAsync(new OrderCreated(...));

Decision Guide

决策指南

ScenarioRecommendation
Module-to-module communication (new project)Wolverine with events (MIT, free)
Module-to-module communication (existing MassTransit)MassTransit (commercial license required from v9)
Reliable event publishingTransactional outbox (both Wolverine and MassTransit support this)
Simple 2-3 step workflowEvent choreography
Complex workflow with compensationWolverine saga or MassTransit saga
Local development brokerRabbitMQ (via Docker or Aspire)
Production cloud brokerAzure Service Bus or RabbitMQ
Want single lib for mediator + messagingWolverine (replaces both Mediator and MassTransit)
场景推荐方案
模块间通信(新项目)使用Wolverine和事件(MIT协议,免费)
模块间通信(已有MassTransit)使用MassTransit(v9+版本需要商业许可证)
可靠事件发布事务性发件箱(Wolverine和MassTransit均支持)
简单2-3步工作流事件编排
带补偿操作的复杂工作流Wolverine Saga或MassTransit Saga
本地开发代理RabbitMQ(通过Docker或Aspire)
生产环境云代理Azure Service Bus或RabbitMQ
想要同时支持中介者和消息传递的单一库Wolverine(替代Mediator和MassTransit)