A dead-simple, fast, and lightweight .NET messaging library for RabbitMQ.
Hare is designed to be minimalistic and intentionally doesn't do a lot of things a fully-fledged service bus or messaging library would. Instead, it focuses on doing one thing well: simple, type-safe message publishing and consuming with RabbitMQ.
Hare is for you if:
- You are using RabbitMQ (the name
Harewouldn't make sense without RabbitMQ anyway) - You are using System.Text.Json for serialization
- You prefer queue-per-type / type-based routing patterns
- You want something simple without the overhead of full-featured service buses
- Fully AOT compatible - Works with Native AOT compilation
- Dead-letter queue support - Automatic DLQ configuration and failed message routing
- Distributed tracing - Built-in OpenTelemetry support with correlation ID propagation
- Aspire integration - Works seamlessly with .NET Aspire for cloud-native development
- Type-safe messaging - Leverage generics for compile-time message type safety
- Minimal dependencies - Only depends on RabbitMQ.Client, OpenTelemetry, and Microsoft.Extensions abstractions
dotnet add package Harepublic record OrderPlacedMessage(
string OrderId,
string CustomerId,
decimal Amount
);
// For AOT compatibility, use System.Text.Json source generators
[JsonSerializable(typeof(OrderPlacedMessage))]
public partial class MessageSerializerContext : JsonSerializerContext { }Configure and send messages from your producer application:
using Hare.Extensions;
using Hare.Contracts;
var builder = WebApplication.CreateBuilder(args);
// Add RabbitMQ connection
builder.Services.AddSingleton<IConnection>(sp =>
{
var factory = new ConnectionFactory { HostName = "localhost" };
return factory.CreateConnectionAsync().GetAwaiter().GetResult();
});
// Or, if you're using .NET Aspire
builder.AddRabbitMQClient("rabbitmq");
// Register Hare with OpenTelemetry support and configure message type
builder.Services
.AddHare()
.AddHareMessage<OrderPlacedMessage>(config =>
{
config.Exchange = "orders";
config.QueueName = "orders.placed";
config.Durable = true;
config.JsonTypeInfo = MessageSerializerContext.Default.OrderPlacedMessage;
}, listen: false);
var app = builder.Build();
// Use the message sender
app.MapPost("/orders", async (
OrderPlacedMessage message,
IMessageSender<OrderPlacedMessage> sender,
CancellationToken ct) =>
{
await sender.SendMessageAsync(message, ct);
return Results.Ok();
});
app.Run();Configure and handle messages in your consumer application:
using Hare.Extensions;
using Hare.Contracts;
var builder = Host.CreateApplicationBuilder(args);
// Add RabbitMQ connection
builder.Services.AddSingleton<IConnection>(sp =>
{
var factory = new ConnectionFactory { HostName = "localhost" };
return factory.CreateConnectionAsync().GetAwaiter().GetResult();
});
// Or, if you're using .NET Aspire
builder.AddRabbitMQClient("rabbitmq");
// Register Hare, configure message type, and register handler
builder.Services
.AddHare()
.AddHareMessage<OrderPlacedMessage>(config =>
{
config.Exchange = "orders";
config.QueueName = "orders.placed";
config.Durable = true;
config.DeadletterExchange = "orders.dead-letter";
config.DeadletterQueueName = "orders.placed.dead-letter";
config.JsonTypeInfo = MessageSerializerContext.Default.OrderPlacedMessage;
}, listen: true)
.AddScoped<IMessageHandler<OrderPlacedMessage>, OrderPlacedHandler>();
var host = builder.Build();
host.Run();public class OrderPlacedHandler(ILogger<OrderPlacedHandler> logger) : IMessageHandler<OrderPlacedMessage>
{
public async ValueTask HandleAsync(OrderPlacedMessage message, CancellationToken cancellationToken)
{
logger.LogInformation("Processing order {OrderId} for customer {CustomerId}",
message.OrderId, message.CustomerId);
// Your business logic here
await Task.CompletedTask;
}
}Hare automatically handles failed message processing with dead-letter queues:
- Automatic DLQ Setup - Both producer and consumer create necessary exchanges and queues
- Requeue Logic - Failed messages are requeued once, then routed to DLQ
- Configuration - Both
DeadletterExchangeandDeadletterQueueNamemust be set together
config.DeadletterExchange = "orders.dead-letter";
config.DeadletterQueueName = "orders.placed.dead-letter";When a message handler throws an exception:
- First failure: Message is nacked and requeued
- Second failure: Message is sent to the dead-letter queue
Hare includes built-in OpenTelemetry support with automatic correlation ID propagation:
builder.Services.AddHare(); // Adds "Hare" ActivitySource
// The library automatically:
// - Sets correlation ID from Activity.Current?.Id when publishing
// - Creates linked activities when consuming messages
// - Traces message processing with proper parent-child relationshipsThis integrates seamlessly with .NET Aspire's dashboard for end-to-end tracing.
Hare is fully compatible with Native AOT compilation. To ensure AOT compatibility:
- Use JSON source generators for serialization:
[JsonSerializable(typeof(OrderPlacedMessage))]
public partial class MessageSerializerContext : JsonSerializerContext { }- Provide
JsonTypeInfowhen configuring messages:
config.JsonTypeInfo = MessageSerializerContext.Default.OrderPlacedMessage;- Use records for immutable messages as shown in the examples above
Contributions are welcome! Please feel free to submit a Pull Request.