Skip to content

luizcarlosfaria/Oragon.RabbitMQ

Repository files navigation

Card

Oragon.RabbitMQ

Minimal APIs for RabbitMQ in .NET — Consume queues with the same MapQueue() pattern you already know from MapPost().

Oragon.RabbitMQ is not HTTP/Kestrel-based. It is a fully custom implementation built on RabbitMQ.Client 7.x natively.


Quality

Quality Gate Status Bugs Code Smells Coverage Duplicated Lines (%) Reliability Rating Security Rating Technical Debt Maintainability Rating Vulnerabilities

Releases

NuGet Version NuGet Downloads GitHub Tag GitHub Release MyGet Version

Project

GitHub Repo stars GitHub last commit Roadmap .NET 8 .NET 9 .NET 10


What is Oragon.RabbitMQ?

If you know ASP.NET Core Minimal APIs, you already know Oragon.RabbitMQ:

// ASP.NET Core — HTTP
app.MapPost("/orders", ([FromServices] OrderService svc, [FromBody] OrderCreated msg) => svc.HandleAsync(msg));

// Oragon.RabbitMQ — AMQP
app.MapQueue("orders", ([FromServices] OrderService svc, [FromBody] OrderCreated msg) => svc.HandleAsync(msg));

It provides everything you need to create resilient RabbitMQ consumers without the need to study numerous books and articles or introduce unknown risks to your environment. All queue consumption settings are configurable through a friendly, fluent, and consistent API.

Why Oragon.RabbitMQ?

  • Minimal API design — familiar MapQueue() pattern, zero learning curve for ASP.NET Core developers
  • RabbitMQ.Client 7.x native — no HTTP, no Kestrel, pure AMQP
  • Near-zero overhead — benchmarks prove 0-1% overhead for I/O-bound workloads
  • Built-in resilience — automatic Ack/Nack/Reject with manual acknowledgment by default
  • DI-first[FromServices], [FromBody], [FromAmqpHeader] attribute binding
  • Pluggable serialization — System.Text.Json or Newtonsoft.Json out of the box
  • Composable flow control — Ack, Nack, Reject, Reply, Forward, and Compose results
  • .NET Aspire integration — first-class support via Oragon.RabbitMQ.AspireClient
  • OpenTelemetry native — via RabbitMQ.Client 7.x built-in instrumentation
  • Multi-framework — targets .NET 8, 9, and 10

Quick Start

Standalone

dotnet add package Oragon.RabbitMQ
dotnet add package Oragon.RabbitMQ.Serializer.SystemTextJson
using Oragon.RabbitMQ;
using Oragon.RabbitMQ.Serializer;
using RabbitMQ.Client;

var builder = Host.CreateApplicationBuilder(args);

// 1. Register consumer infrastructure
builder.AddRabbitMQConsumer();

// 2. Register serializer
builder.Services.AddAmqpSerializer(options: JsonSerializerOptions.Default);

// 3. Register RabbitMQ connection
builder.Services.AddSingleton<IConnectionFactory>(sp => new ConnectionFactory()
{
    Uri = new Uri("amqp://guest:guest@localhost:5672"),
    DispatchConsumersAsync = true
});
builder.Services.AddSingleton(sp =>
    sp.GetRequiredService<IConnectionFactory>().CreateConnectionAsync().GetAwaiter().GetResult());

// 4. Register your service
builder.Services.AddSingleton<OrderService>(); // singleton, scoped or transient

var app = builder.Build();

Example 1

// 5. Map queue to handler
app.MapQueue("orders", ([FromServices] OrderService svc, OrderCreated msg) =>
    svc.HandleAsync(msg));

app.Run();

Example 2

Asume the controller has a method CanProcess that returns a boolean.

app.MapQueue("orders", async ([FromServices] OrderService svc, OrderCreated msg) =>
{
    if (svc.CanProcess(msg))
    {
        await svc.HandleAsync(msg);
        return AmqpResults.Ack();
    }
    return AmqpResults.Nack(requeue: true);
});

Example 3

You can also handle exceptions yourself and return a valid IAmqpResult:

app.MapQueue("orders", async ([FromServices] OrderService svc, OrderCreated msg) =>
{
    try
    {
        await svc.HandleAsync(msg);
        return AmqpResults.Ack();
    }
    catch (Exception ex)
    {
        // Log the exception
        return AmqpResults.Nack(requeue: true);
    }
});

With .NET Aspire

Replace Aspire.RabbitMQ.Client with Oragon.RabbitMQ.AspireClient to get RabbitMQ.Client 7.x support:

dotnet add package Oragon.RabbitMQ.AspireClient
builder.AddRabbitMQClient("rabbitmq");

After Aspire.RabbitMQ.Client gains RabbitMQ.Client 7.x support, the Oragon.RabbitMQ.AspireClient package will be deprecated.

Packages

Package Purpose
Oragon.RabbitMQ Core library — consumer infrastructure, MapQueue, flow control
Oragon.RabbitMQ.Abstractions Interfaces (IAmqpResult, IAmqpSerializer, IAmqpContext)
Oragon.RabbitMQ.Serializer.SystemTextJson System.Text.Json serializer
Oragon.RabbitMQ.Serializer.NewtonsoftJson Newtonsoft.Json serializer
Oragon.RabbitMQ.AspireClient .NET Aspire integration (RabbitMQ.Client 7.x)

Configuration Reference

All configuration is done through fluent methods on the ConsumerDescriptor returned by MapQueue():

Method Description Default
.WithPrefetch(ushort) Number of messages prefetched from the broker 1
.WithDispatchConcurrency(ushort) Concurrent message processing slots 1
.WithConsumerTag(string) Custom consumer tag auto-generated
.WithExclusive(bool) Exclusive consumer on the queue false
.WithTopology(Func<IChannel, CancellationToken, Task>) Declare exchanges/queues/bindings on startup none
.WithConnection(Func<IServiceProvider, CancellationToken, Task<IConnection>>) Custom connection factory IConnection from DI
.WithSerializer(Func<IServiceProvider, IAmqpSerializer>) Custom serializer factory IAmqpSerializer from DI
.WithChannel(Func<IConnection, CancellationToken, Task<IChannel>>) Custom channel factory auto-created
.WhenSerializationFail(Func<IAmqpContext, Exception, IAmqpResult>) Behavior on deserialization errors Reject(requeue: false)
.WhenProcessFail(Func<IAmqpContext, Exception, IAmqpResult>) Behavior on handler exceptions Nack(requeue: false)
app.MapQueue("orders", ([FromServices] OrderService svc, OrderCreated msg) =>
    svc.HandleAsync(msg))
    .WithPrefetch(100)
    .WithDispatchConcurrency(8)
    .WhenProcessFail((ctx, ex) => AmqpResults.Nack(requeue: true));

Flow Control

By default, Oragon handles acknowledgments automatically:

  • SuccessBasicAck
  • Serialization failureBasicReject (no requeue) — use dead-lettering
  • Processing failureBasicNack (no requeue) — use dead-lettering

For explicit control, return an IAmqpResult from your handler:

Group Method Description
Basic AmqpResults.Ack() Acknowledge the message
AmqpResults.Nack(requeue) Negative acknowledge
AmqpResults.Reject(requeue) Reject the message
RPC AmqpResults.Reply<T>(T) Reply to the caller
AmqpResults.ReplyAndAck<T>(T) Reply and acknowledge
Routing AmqpResults.Forward<T>(exchange, routingKey, mandatory, params T[]) Forward to another exchange
AmqpResults.ForwardAndAck<T>(exchange, routingKey, mandatory, params T[]) Forward and acknowledge
Composition AmqpResults.Compose(params IAmqpResult[]) Combine multiple results

Model Binding

Attributes

Attribute Resolves from
[FromServices] DI container (supports keyed services)
[FromBody] Deserialized message body
[FromAmqpHeader("key")] AMQP message header by key

Auto-bound Types

These types are resolved automatically by the model binder without any attribute:

Type Value
IConnection Current RabbitMQ connection
IChannel Current RabbitMQ channel
BasicDeliverEventArgs Raw delivery event
DeliveryModes Message delivery mode
IReadOnlyBasicProperties Message properties
IServiceProvider Scoped service provider
IAmqpContext Full AMQP context
CancellationToken Cancellation token

Auto-bound String Parameters

String parameters are matched by name convention:

Parameter names Value
queue, queueName Name of the consumed queue
routing, routingKey Message routing key
exchange, exchangeName Source exchange name
consumer, consumerTag Consumer tag

Telemetry

RabbitMQ.Client 7.x implements native OpenTelemetry instrumentation via System.Diagnostics.ActivitySource. Your existing OpenTelemetry collectors will capture AMQP operations automatically without any additional configuration in this library.

Benchmarks

All benchmarks compare Oragon.RabbitMQ against hand-written native RabbitMQ.Client code performing the same DI scoping, serialization, try/catch, and ack/nack logic.

Environment: AMD Ryzen 9 9950X3D (16 cores / 32 threads), .NET 9.0.12, Windows 11, GC Server=True, BenchmarkDotNet v0.14.0.

Performance Summary

Benchmark Scenario Oragon Overhead Verdict
Concurrency Scaling I/O-Bound (1000 msgs, Task.Delay) 0 - 1% Excellent - Zero overhead
Concurrency Scaling CPU-Bound (1000 msgs, HashCode loop) 2 - 8% Very Good
Throughput NoOp handler (1000-5000 msgs) 0 - 11% Good
Throughput CPU-Bound handler 0 - 14% Good
Latency Single message (all handlers) 5 - 7% (~3.5 ms fixed) Good
Allocation Large messages (100 msgs) 9% time, 1% memory Excellent
RPC ReplyAndAck vs native dedicated -7% (Oragon wins) Excellent

RPC Performance

Size Native Dedicated (ms) Oragon ReplyAndAck (ms) Ratio
Small 50.1 46.8 0.93
Medium 50.4 47.1 0.93

Oragon is 7% faster and allocates 17% less memory for RPC by reusing pre-warmed infrastructure.

Memory Allocation Summary

Scenario Oragon Overhead Context
Large messages ~1% Message body dominates
Small messages (bulk) ~20% Fixed DI scope + pipeline cost
Single message latency 2 - 3x Fixed overhead dominates
RPC 17% less Reuses pre-warmed infrastructure

Concurrency Scaling (I/O-Bound)

1000 messages with Task.Delay(5) handler. This is the most representative scenario for real-world workloads.

Prefetch Concurrency Native (ms) Oragon (ms) Ratio
10 2 2,700 2,706 1.00
10 4 1,380 1,390 1.01
10 8 728 731 1.00
50 4 1,351 1,357 1.00
50 8 694 694 1.00
100 4 1,347 1,352 1.00
100 8 677 681 1.01

Conclusion: Ratio consistently between 0.98 - 1.01. Zero latency overhead for I/O-bound workloads.

Key Takeaways

  1. I/O-bound workloads (the most common real-world scenario) — zero measurable overhead.
  2. CPU-bound with small messages (worst case) — 5-10% overhead from DI scope + dispatch pipeline.
  3. RPC — Oragon is faster and more memory-efficient than hand-written code.
  4. The overhead is predictable and constant: ~3.5 ms fixed per message + ~2.5 KB of fixed allocation. In production workloads with larger messages and heavier handlers, this fixed cost becomes irrelevant.
  5. Fair trade-off: For ~5% overhead in the worst case, Oragon provides: integrated DI, pluggable serialization, automatic error handling, declarative flow control, and a clean API.

Full benchmark results are available in benchmarks/Oragon.RabbitMQ.Benchmarks/BenchmarkDotNet.Artifacts/results/.

Design Philosophy

Decoupling Business Logic from Infrastructure

Oragon.RabbitMQ is designed to decouple RabbitMQ consumers from business logic. Your business code remains completely unaware of the queue consumption context — resulting in simple, decoupled, agnostic, reusable, and highly testable code.

Manual Acknowledgment by Default

This consumer is focused on creating resilient consumers using manual acknowledgments (autoAck: false). The automatic flow handles Ack/Nack/Reject so you don't have to, but you can take control at any time by returning IAmqpResult.

Dead-Lettering as Recommended Pattern

Both serialization failures (Reject) and processing failures (Nack) default to no requeue. This is intentional — configure dead-letter exchanges on your queues to capture failed messages for inspection, replay, or alerting.

Samples

Tech

C# .Net Visual Studio Jenkins Telegram

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

License

MIT — LUIZ CARLOS FARIA - ACADEMIA.DEV - MENSAGERIA.NET

About

Consume RabbitMQ Queues like Minimal API's

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages