{"slug": "masstransit-vs-wolverine-vs-raw-rabbitmq-client-after-the-license-change", "title": "MassTransit vs Wolverine vs Raw RabbitMQ.Client After the License Change", "summary": "MassTransit v9 (9.2.3 on NuGet) now requires a commercial license with a runtime key on every machine running MassTransit code, including developer laptops, while v8 and earlier remain free under Apache 2.0 but unsupported under the commercial agreement, according to a Softinbit Technology comparison of MassTransit, Wolverine 6 and RabbitMQ.Client 7. The article benchmarks three paths for the same order-processing flow — staying on MassTransit v8 (8.5.11) or licensing v9, migrating to MIT-licensed Wolverine 6 (6.50.0, .NET 9 and 10 only), or writing async consumers directly against RabbitMQ.Client 7 (7.2.2, dual-licensed Apache 2.0 and MPL 2.0) — with retries, error queues and idempotent handling. It recommends MassTransit v9 with a license for teams using sagas, routing slips, the transactional outbox or several transports, and raw RabbitMQ.Client for one or two message types where the team accepts owning retries, error queues, serialization and topology.", "body_md": "Decide whether to license MassTransit v9, stay on v8 for now, move to Wolverine, or write the RabbitMQ code yourself.\n\nNote: this article was drafted with AI assistance and reviewed by Softinbit Technology before publishing. It is part of the series The .NET Field Guide.\n\nTL;DR — MassTransit v9 moved to a commercial license requiring runtime keys. If you only publish and consume events, you do not need to blindly pay or panic. — Compare three real paths for the same order-processing flow: keep MassTransit (v8 open source or v9 licensed), migrate to MIT-licensed Wolverine 6, or write lightweight async consumers with RabbitMQ.Client 7. — Complete benchmarks, topology setups, retry/dead-letter policies, and an outbox comparison to pick the right option for your team.\n\nLevel: Intermediate · Prerequisites: Basic understanding of message queues and RabbitMQ, ASP.NET Core dependency injection · Reading time: 14 min\n\nDependabot opened a pull request this morning: MassTransit.RabbitMQ 8.5.11 to 9.2.3. The build is green, but the MassTransit docs now say v9 is a commercial product and that the license key must be available at runtime on every machine that runs MassTransit code, developer laptops included. Your Store.Api uses MassTransit for exactly one flow: when an order is placed, publish OrderPlaced, and a consumer reserves stock.\n\nYou can keep MassTransit (v8 for now, or v9 with a license), move to Wolverine, or use RabbitMQ.Client directly. This article builds the same flow in all three, with retries, an error queue, and idempotent handling.\n\nThe short answer\n\nChoose MassTransit v9 with a license when you use sagas, routing slips, the transactional outbox, or several transports, and the subscription cost is smaller than the rewrite. Small organizations should check whether they qualify for the full discount.\n\nChoose MassTransit v8 when you need the current code to keep running this quarter, and you put a dated migration or licensing decision on the roadmap.\n\nChoose Wolverine when you want an MIT-licensed framework with retries, dead-letter queues, an inbox and outbox, and convention-based handlers, and you run on .NET 9 or 10.\n\nChoose raw RabbitMQ.Client when you have one or two message types, want no framework dependency, and accept that you own retries, error queues, serialization, and topology.\n\nWhat is being compared\n\nMassTransit is a message bus framework for .NET with consumers, sagas, retries, an outbox, and testing helpers. The current major version is 9 (9.2.3 on NuGet in October 2026), sold by Massient, Inc., the company created by founder Chris Patterson. Version 9 needs a license; the license terms say v8 and earlier “remain free and open source under their original licenses” (Apache 2.0) but are unsupported under the commercial agreement. Version 8 still ships patch releases (8.5.11 at the end of September 2026).\n\nWolverine (WolverineFx) is a messaging and command-handling framework from JasperFx Software, the team behind Marten. The current major is 6 (6.50.0), released under the MIT license. Wolverine 6 targets .NET 9 and .NET 10 only; teams on .NET 8 must stay on 5.x.\n\nRabbitMQ.Client is the official .NET client for RabbitMQ, maintained by the RabbitMQ core team. The current major is 7 (7.2.2), dual-licensed under Apache 2.0 and the Mozilla Public License 2.0. Version 7 made the API fully asynchronous.\n\nThe same task in each option\n\nThe task: POST /api/orders/{id}/placed-event publishes OrderPlaced. A consumer reserves stock for each line. Transient failures retry three times with a delay. Permanent failures (a product that does not exist) go straight to an error queue. A redelivered message never reserves stock twice.\n\nStart RabbitMQ locally:\n\n```\ndocker run -d --name store-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:4-management\n```\n\nAdd the settings to appsettings.Development.json:\n\n```\n{  \"RabbitMq\": { \"Host\": \"localhost\", \"Username\": \"guest\", \"Password\": \"guest\" }}\n```\n\nShared code used by all three options\n\nThe contract, the idempotency table, and the business logic are identical everywhere.\n\n```\n// File: Orders/Messaging/OrderPlaced.csnamespace Store.Api.Orders.Messaging;\npublic sealed record OrderPlaced(int OrderId, string UserId, DateTime PlacedAtUtc, IReadOnlyList<OrderPlacedLine> Lines);\npublic sealed record OrderPlacedLine(int ProductId, int Quantity);\npublic interface IOrderEvents{    Task PublishOrderPlacedAsync(OrderPlaced message, CancellationToken ct);}\npublic sealed class ProcessedMessage{    public string Consumer { get; set; } = \"\";    public string MessageKey { get; set; } = \"\";    public DateTime ProcessedAtUtc { get; set; }}\n```\n\nRegister the table in AppDbContext (in Store.Api.Data) and add a migration:\n\n```\n// File: Data/AppDbContext.cs (additions; add using Store.Api.Orders.Messaging;)public DbSet<ProcessedMessage> ProcessedMessages => Set<ProcessedMessage>();\njs\n// Inside OnModelCreating:modelBuilder.Entity<ProcessedMessage>(e =>{    e.HasKey(p => new { p.Consumer, p.MessageKey });    e.Property(p => p.Consumer).HasMaxLength(100);    e.Property(p => p.MessageKey).HasMaxLength(100);});\n// File: Orders/Messaging/StockReservationService.csusing Microsoft.EntityFrameworkCore;using Store.Api.Data;\nnamespace Store.Api.Orders.Messaging;\npublic sealed class StockReservationService(AppDbContext db, TimeProvider time, ILogger<StockReservationService> logger){    private const string ConsumerName = \"reserve-stock\";\npublic async Task ReserveAsync(OrderPlaced message, CancellationToken ct)    {        var key = $\"order-{message.OrderId}\";        if (await db.ProcessedMessages.AnyAsync(p => p.Consumer == ConsumerName && p.MessageKey == key, ct))        {            logger.LogInformation(\"Stock for order {OrderId} already reserved, skipping duplicate\", message.OrderId);            return;        }\njs\n        await using var tx = await db.Database.BeginTransactionAsync(ct);        foreach (var line in message.Lines)        {            // Atomic decrement: two orders reserving the same product cannot overwrite each other.            var updated = await db.Products.Where(p => p.Id == line.ProductId)                .ExecuteUpdateAsync(s => s.SetProperty(p => p.Stock, p => p.Stock - line.Quantity), ct);            if (updated == 0)                throw new InvalidOperationException($\"Product {line.ProductId} does not exist.\");        }\ndb.ProcessedMessages.Add(new ProcessedMessage        {            Consumer = ConsumerName,            MessageKey = key,            ProcessedAtUtc = time.GetUtcNow().UtcDateTime        });        await db.SaveChangesAsync(ct);        await tx.CommitAsync(ct);    }}\n```\n\nThe stock updates and the ProcessedMessages row commit in one transaction, so they succeed or fail together. If two copies of the same message race, the composite primary key rejects the second insert with a DbUpdateException, the retry policy runs it again, and the second attempt sees the row and skips. That is why the key is the business identity (order-42) and not a transport message id.\n\n```\n// File: Orders/OrderEventsController.csusing Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Store.Api.Data;using Store.Api.Orders.Messaging;\nnamespace Store.Api.Orders;\n[ApiController][Route(\"api/orders\")]public sealed class OrderEventsController(AppDbContext db, IOrderEvents events) : ControllerBase{    [HttpPost(\"{id:int}/placed-event\")]    public async Task<IActionResult> PublishPlaced(int id, CancellationToken ct)    {        var order = await db.Orders.AsNoTracking().Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == id, ct);        if (order is null) return NotFound();\njs\n        var lines = order.Lines.Select(l => new OrderPlacedLine(l.ProductId, l.Quantity)).ToList();        await events.PublishOrderPlacedAsync(new OrderPlaced(order.Id, order.UserId, order.CreatedAtUtc, lines), ct);        return Accepted();    }}\n```\n\nAll three options also need builder.Services.AddSingleton(TimeProvider.System); and builder.Services.AddScoped<StockReservationService>(); in Program.cs.\n\nMassTransit v8\n\n```\ndotnet add package MassTransit.RabbitMQ --version 8.5.11\n```\n\nPin the version. A plain dotnet add package MassTransit.RabbitMQ now installs v9.\n\n```\n// File: Orders/Messaging/MassTransitBus/ReserveStockConsumer.csusing MassTransit;\nnamespace Store.Api.Orders.Messaging.MassTransitBus;\npublic sealed class ReserveStockConsumer(StockReservationService reservations) : IConsumer<OrderPlaced>{    public Task Consume(ConsumeContext<OrderPlaced> context) =>        reservations.ReserveAsync(context.Message, context.CancellationToken);}\npublic sealed class ReserveStockConsumerDefinition : ConsumerDefinition<ReserveStockConsumer>{    public ReserveStockConsumerDefinition()    {        EndpointName = \"order-placed.reserve-stock\";        ConcurrentMessageLimit = 10;    }\nprotected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator,        IConsumerConfigurator<ReserveStockConsumer> consumerConfigurator, IRegistrationContext context)    {        endpointConfigurator.UseMessageRetry(r =>        {            r.Ignore<InvalidOperationException>();            r.Intervals(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));        });    }}\npublic sealed class MassTransitOrderEvents(IPublishEndpoint publishEndpoint) : IOrderEvents{    public Task PublishOrderPlacedAsync(OrderPlaced message, CancellationToken ct) =>        publishEndpoint.Publish(message, ct);}\n// Program.cs, before builder.Build() (add using MassTransit; and using Store.Api.Orders.Messaging.MassTransitBus;)var rabbit = builder.Configuration.GetSection(\"RabbitMq\");builder.Services.AddScoped<IOrderEvents, MassTransitOrderEvents>();builder.Services.AddMassTransit(x =>{    x.AddConsumer<ReserveStockConsumer, ReserveStockConsumerDefinition>();    x.UsingRabbitMq((context, cfg) =>    {        cfg.Host(rabbit[\"Host\"]!, \"/\", h =>        {            h.Username(rabbit[\"Username\"]!);            h.Password(rabbit[\"Password\"]!);        });        cfg.ConfigureEndpoints(context);    });});\n```\n\nConfigureEndpoints creates the queue order-placed.reserve-stock, an exchange per message type, and the bindings. You never declare topology.\n\nUseMessageRetry retries in memory three times. Ignore<InvalidOperationException>() skips retries for the permanent error.\n\nWhen retries run out, MassTransit moves the message to order-placed.reserve-stock_error with the exception details in headers, and publishes a Fault<OrderPlaced> you can consume.\n\nDelayed redelivery (minutes, not seconds) is a separate feature that needs a message scheduler, such as the RabbitMQ delayed message exchange plugin.\n\nWolverine\n\n```\ndotnet add package WolverineFxdotnet add package WolverineFx.RabbitMQdotnet add package WolverineFx.RuntimeCompilation\n```\n\nSince 6.0, runtime code generation is opt-in: add WolverineFx.RuntimeCompilation. It registers itself; opts.UseRuntimeCompilation() is needed only when extension auto-discovery is off.\n\n```\n// File: Orders/Messaging/WolverineBus/OrderPlacedHandler.csusing Wolverine;\nnamespace Store.Api.Orders.Messaging.WolverineBus;\npublic sealed class OrderPlacedHandler{    public Task Handle(OrderPlaced message, StockReservationService reservations, CancellationToken cancellationToken) =>        reservations.ReserveAsync(message, cancellationToken);}\npublic sealed class WolverineOrderEvents(IMessageBus bus) : IOrderEvents{    public Task PublishOrderPlacedAsync(OrderPlaced message, CancellationToken ct) =>        bus.PublishAsync(message).AsTask();}\n// Program.cs, before builder.Build()// (add using Wolverine; using Wolverine.ErrorHandling; using Wolverine.RabbitMQ; using Store.Api.Orders.Messaging.WolverineBus;)var rabbit = builder.Configuration.GetSection(\"RabbitMq\");builder.Services.AddScoped<IOrderEvents, WolverineOrderEvents>();builder.Host.UseWolverine(opts =>{    opts.UseRuntimeCompilation(); // from WolverineFx.RuntimeCompilation    opts.UseRabbitMq(f =>        {            f.HostName = rabbit[\"Host\"]!;            f.UserName = rabbit[\"Username\"]!;            f.Password = rabbit[\"Password\"]!;        })        .AutoProvision();\nopts.PublishMessage<OrderPlaced>()        .ToRabbitExchange(\"store.orders\", exchange => exchange.BindQueue(\"order-placed.reserve-stock\"));\nopts.ListenToRabbitQueue(\"order-placed.reserve-stock\")        .DeadLetterQueueing(new DeadLetterQueue(\"order-placed.reserve-stock.error\"));\nopts.Policies.OnException<InvalidOperationException>().MoveToErrorQueue();    opts.Policies.OnAnyException()        .RetryWithCooldown(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15))        .Then.MoveToErrorQueue();});\n```\n\nNo consumer interface. Wolverine finds public classes whose names end in Handler or Consumer and methods named Handle or Consume. Extra parameters (StockReservationService, CancellationToken) are injected per message.\n\nError rules are checked in order and the first match wins, so the specific rule comes before OnAnyException(). A message that matches no rule goes to the error queue.\n\nBy default Wolverine uses RabbitMQ’s native dead-letter exchange and a shared wolverine-dead-letter-queue. DeadLetterQueueing gives this queue its own error queue.\n\nWolverine’s durable inbox (with message storage such as WolverineFx.SqlServer) adds deduplication by envelope id. The business-key check above still matters, because a client that publishes the same order twice creates two envelopes.\n\nRaw RabbitMQ.Client 7\n\n```\ndotnet add package RabbitMQ.Client\n```\n\nNow you write what the frameworks did: a long-lived connection, topology, publisher confirms, a consumer loop, and retries. Retries go to a queue with a 10-second message time to live (TTL); when it expires, RabbitMQ dead-letters the message back to the main queue.\n\n```\n// File: Orders/Messaging/RawRabbit/RabbitConnection.csusing RabbitMQ.Client;\nnamespace Store.Api.Orders.Messaging.RawRabbit;\npublic sealed class RabbitConnection(IConfiguration configuration) : IAsyncDisposable{    private readonly SemaphoreSlim _gate = new(1, 1);    private IConnection? _connection;\npublic async Task<IConnection> GetAsync(CancellationToken ct)    {        if (_connection is not null) return _connection;        await _gate.WaitAsync(ct);        try        {            var section = configuration.GetSection(\"RabbitMq\");            _connection ??= await new ConnectionFactory            {                HostName = section[\"Host\"]!,                UserName = section[\"Username\"]!,                Password = section[\"Password\"]!            }.CreateConnectionAsync(\"Store.Api\", ct);            return _connection;        }        finally { _gate.Release(); }    }\npublic async ValueTask DisposeAsync()    {        if (_connection is not null) await _connection.DisposeAsync();    }}\njs\npublic static class RabbitTopology{    public const string Exchange = \"store.orders\";    public const string RoutingKey = \"order.placed\";    public const string Queue = \"order-placed.reserve-stock\";    public const string RetryQueue = Queue + \".retry\";    public const string ErrorQueue = Queue + \".error\";\npublic static readonly CreateChannelOptions Confirmed =        new(publisherConfirmationsEnabled: true, publisherConfirmationTrackingEnabled: true);\npublic static async Task DeclareAsync(IChannel channel, CancellationToken ct)    {        await channel.ExchangeDeclareAsync(Exchange, ExchangeType.Topic, durable: true, cancellationToken: ct);        await channel.QueueDeclareAsync(Queue, durable: true, exclusive: false, autoDelete: false, cancellationToken: ct);        await channel.QueueBindAsync(Queue, Exchange, RoutingKey, cancellationToken: ct);        await channel.QueueDeclareAsync(RetryQueue, durable: true, exclusive: false, autoDelete: false,            arguments: new Dictionary<string, object?>            {                [\"x-message-ttl\"] = 10_000,                [\"x-dead-letter-exchange\"] = \"\",                [\"x-dead-letter-routing-key\"] = Queue            }, cancellationToken: ct);        await channel.QueueDeclareAsync(ErrorQueue, durable: true, exclusive: false, autoDelete: false, cancellationToken: ct);    }}\n// File: Orders/Messaging/RawRabbit/RabbitOrderEvents.csusing System.Text.Json;using RabbitMQ.Client;\nnamespace Store.Api.Orders.Messaging.RawRabbit;\npublic sealed class RabbitOrderEvents(RabbitConnection rabbit) : IOrderEvents, IAsyncDisposable{    private readonly SemaphoreSlim _gate = new(1, 1);    private IChannel? _channel;\npublic async Task PublishOrderPlacedAsync(OrderPlaced message, CancellationToken ct)    {        await _gate.WaitAsync(ct);        try        {            if (_channel is null)            {                _channel = await (await rabbit.GetAsync(ct)).CreateChannelAsync(RabbitTopology.Confirmed, ct);                await RabbitTopology.DeclareAsync(_channel, ct);            }\njs\n            var properties = new BasicProperties            {                Persistent = true,                ContentType = \"application/json\",                MessageId = $\"order-{message.OrderId}\",                Type = nameof(OrderPlaced)            };            await _channel.BasicPublishAsync(RabbitTopology.Exchange, RabbitTopology.RoutingKey, mandatory: true,                basicProperties: properties, body: JsonSerializer.SerializeToUtf8Bytes(message), cancellationToken: ct);        }        finally { _gate.Release(); }    }\npublic async ValueTask DisposeAsync()    {        if (_channel is not null) await _channel.DisposeAsync();    }}\n// File: Orders/Messaging/RawRabbit/ReserveStockWorker.csusing System.Text.Json;using RabbitMQ.Client;using RabbitMQ.Client.Events;\nnamespace Store.Api.Orders.Messaging.RawRabbit;\npublic sealed class ReserveStockWorker(RabbitConnection rabbit, IServiceScopeFactory scopes,    ILogger<ReserveStockWorker> logger) : BackgroundService{    private const int MaxRetries = 3;    private const string RetryHeader = \"x-retry-count\";    private IChannel? _consumeChannel;    private IChannel? _publishChannel;\nprotected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        var connection = await rabbit.GetAsync(stoppingToken);        _publishChannel = await connection.CreateChannelAsync(RabbitTopology.Confirmed, stoppingToken);        _consumeChannel = await connection.CreateChannelAsync(cancellationToken: stoppingToken);        await RabbitTopology.DeclareAsync(_consumeChannel, stoppingToken);        await _consumeChannel.BasicQosAsync(prefetchSize: 0, prefetchCount: 10, global: false, stoppingToken);\njs\n        var consumer = new AsyncEventingBasicConsumer(_consumeChannel);        consumer.ReceivedAsync += HandleAsync;        await _consumeChannel.BasicConsumeAsync(RabbitTopology.Queue, autoAck: false, consumer, stoppingToken);\ntry { await Task.Delay(Timeout.Infinite, stoppingToken); }        catch (OperationCanceledException) { }    }\nprivate async Task HandleAsync(object sender, BasicDeliverEventArgs delivery)    {        try        {            var message = JsonSerializer.Deserialize<OrderPlaced>(delivery.Body.Span)                ?? throw new InvalidOperationException(\"Empty OrderPlaced message.\");            await using var scope = scopes.CreateAsyncScope();            await scope.ServiceProvider.GetRequiredService<StockReservationService>()                .ReserveAsync(message, delivery.CancellationToken);        }        catch (Exception ex)        {            var attempt = ReadRetryCount(delivery.BasicProperties) + 1;            var permanent = ex is JsonException or InvalidOperationException;            var target = permanent || attempt > MaxRetries ? RabbitTopology.ErrorQueue : RabbitTopology.RetryQueue;            logger.LogWarning(ex, \"OrderPlaced failed on attempt {Attempt}, moving it to {Queue}\", attempt, target);\njs\n            var properties = new BasicProperties(delivery.BasicProperties)            {                Headers = new Dictionary<string, object?>(delivery.BasicProperties.Headers ?? new Dictionary<string, object?>())                {                    [RetryHeader] = attempt,                    [\"x-last-error\"] = ex.Message                }            };            await _publishChannel!.BasicPublishAsync(\"\", target, mandatory: true, basicProperties: properties, body: delivery.Body);        }\nawait _consumeChannel!.BasicAckAsync(delivery.DeliveryTag, multiple: false);    }\njs\n    private static int ReadRetryCount(IReadOnlyBasicProperties properties) =>        properties.Headers is not null && properties.Headers.TryGetValue(RetryHeader, out var value)            ? value switch { int i => i, long l => (int)l, _ => 0 }            : 0;}\n// Program.cs, before builder.Build() (add using Store.Api.Orders.Messaging.RawRabbit;)builder.Services.AddSingleton<RabbitConnection>();builder.Services.AddSingleton<IOrderEvents, RabbitOrderEvents>();builder.Services.AddHostedService<ReserveStockWorker>();\n```\n\nPublisher confirms make BasicPublishAsync wait for the broker to accept the message, and mandatory: true turns \"no queue bound\" into an exception instead of a silent drop.\n\nThe failed copy is republished on a separate confirmed channel before the original is acknowledged. If the process dies between those two steps, the message is delivered twice, and the idempotency check absorbs it.\n\nPublish order 42 twice with any option and the logs show the guard working:\n\nLearning curve: MassTransit: Medium (large feature set) | Wolverine: Medium (conventions & code generation) | RabbitMQ.Client: Low API surface, but requires deep AMQP knowledge\n\nYou run MassTransit sagas or routing slips in production. Price the v9 license first. Rewriting state machines costs more than most subscriptions, and v9 includes the fixes from the v8 codebase.\n\nYou use MassTransit only for publish and consume. Wolverine is the shortest move: the handler is smaller than the consumer, and retry rules map almost one to one.\n\nYou are still on .NET 8 and cannot upgrade soon. Stay on MassTransit v8 or use Wolverine 5.x; Wolverine 6 will not install.\n\nYou have one integration and a team that knows RabbitMQ. Raw RabbitMQ.Client is fine, but budget for the parts this article skipped: recovery logging, metrics, and tests for the retry path.\n\nYour company is small. Massient states that organizations under 1 million US dollars in gross annual revenue may qualify for a full discount on the license (commercial support excluded). Check the current terms before you decide on cost alone.\n\nSwitching later\n\nWhat changes if you stay on MassTransit v8\n\nPin the major version so tooling does not upgrade you by accident:\n\n```\n<PackageReference Include=\"MassTransit.RabbitMQ\" Version=\"[8.5.11,9.0)\" />\n# .github/dependabot.yml (inside the nuget update entry)ignore:  - dependency-name: \"MassTransit*\"    update-types: [\"version-update:semver-major\"]\n```\n\nPlan for the end of fixes. When v9 was announced in 2025, the plan was security patches and critical fixes for v8 through the end of 2026. After that, assume no fixes and check the MassTransit support page for the current statement.\n\nUpgrading to v9 later is mostly mechanical. The v9 upgrade notes list TransactionalBus removal, per-bus JSON settings, AddSendEndpointConvention renamed to AddMessageRoute, and changed outbox tables in 9.1. You also add the license through MT_LICENSE, MT_LICENSE_PATH, cfg.SetLicense(...), or cfg.SetLicenseLocation(...).\n\nMassTransit to Wolverine\n\nMove one consumer at a time. Wolverine can read and write MassTransit’s envelope format on dedicated endpoints with .UseMassTransitInterop() on ListenToRabbitQueue(...) and ToRabbitExchange(...).\n\nRename consumers to handlers: Consume(ConsumeContext<T>) becomes Handle(T message, ...dependencies).\n\nQueues that MassTransit declared do not have Wolverine’s x-dead-letter-exchange argument, and RabbitMQ refuses a redeclare with different arguments. Use new DeadLetterQueue(\"name\", DeadLetterQueueMode.InteropFriendly) or new queue names.\n\nRaw client to a framework\n\nYour messages are plain JSON with no envelope. MassTransit reads them with e.UseRawJsonDeserializer() on the receive endpoint; Wolverine uses .DefaultIncomingMessage<OrderPlaced>() on the listener.\n\nDrain the custom .retry queue before you delete it, or messages waiting there will dead-letter into a queue nobody reads.\n\nCommon pitfalls\n\nPublishing after SaveChangesAsync without an outbox. If the broker is down, the order exists and the event is lost. Use MassTransit's or Wolverine's outbox, or the pattern in the playbook guide on the transactional outbox.\n\nRetrying permanent errors. A missing product will fail on every attempt. Mark it as permanent (Ignore<T>, MoveToErrorQueue, or the permanent check) so it reaches the error queue in seconds, not minutes.\n\nIdempotency keyed on transport ids. MassTransit MessageId and Wolverine envelope ids change when a client publishes twice. Key the dedupe table on the business identity.\n\nWolverine startup cost and missing handlers. Runtime code generation compiles handlers with Roslyn. For production, pre-generate with dotnet run -- codegen write and set opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Static. If a handler is never called, print opts.DescribeHandlerMatch(typeof(OrderPlacedHandler)).\n\nForgetting the v9 license on build agents. The key must be present wherever MassTransit runs, which includes integration tests on continuous integration agents.\n\nRelated reading\n\nRabbitMQ vs Kafka vs Azure Service Bus for .NET Teams\n\nWolverine for .NET: Messaging and Command Handling in One Library\n\nRabbitMQ Dead Letter Exchanges and Delayed Retries with .NET\n\nLost Messages and the Outbox That Fixed Them\n\nThe Transactional Outbox Pattern in .NET (in “The .NET Implementation Playbook”)", "url": "https://wpnews.pro/news/masstransit-vs-wolverine-vs-raw-rabbitmq-client-after-the-license-change", "canonical_source": "https://blog.devgenius.io/masstransit-vs-wolverine-vs-raw-rabbitmq-client-after-the-license-change-0e7a8508780a?source=rss----4e2c1156667e---4", "published_at": "2026-11-04 16:17:59+00:00", "updated_at": "2026-11-04 16:49:21.309885+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["MassTransit", "Wolverine", "RabbitMQ.Client", "Softinbit Technology", "JasperFx Software", "Chris Patterson", "Massient, Inc.", "Marten"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/masstransit-vs-wolverine-vs-raw-rabbitmq-client-after-the-license-change", "markdown": "https://wpnews.pro/news/masstransit-vs-wolverine-vs-raw-rabbitmq-client-after-the-license-change.md", "text": "https://wpnews.pro/news/masstransit-vs-wolverine-vs-raw-rabbitmq-client-after-the-license-change.txt", "jsonld": "https://wpnews.pro/news/masstransit-vs-wolverine-vs-raw-rabbitmq-client-after-the-license-change.jsonld"}}