EG With masstransit and hangfire
PRODUCER + CONSUMER CHECKPOINT
(SINGLE END-TO-END EXAMPLE — MassTransit + RabbitMQ + Hangfire)
SCENARIO
Invoice is created.
We must asynchronously send it to Peppol.
HTTP must return immediately.
RabbitMQ is the transport.
MassTransit manages topology.
Hangfire executes the slow work.
1. MESSAGE (INTENT)¶
// Command: obligation, must be handled
public record SendInvoiceToPeppol(Guid InvoiceId);
-
This is a command
-
Exactly one handler is expected
-
Failure is critical
2. PRODUCER (API SIDE)¶
[ApiController]
[Route("invoice")]
public class InvoiceController : ControllerBase
{
private readonly ISendEndpointProvider _send;
public InvoiceController(ISendEndpointProvider send)
{
_send = send;
}
[HttpPost("{invoiceId}/create")]
public async Task<IActionResult> Create(Guid invoiceId)
{
// sync work (DB insert etc.)
// invoice is now created
var endpoint = await _send.GetSendEndpoint(
new Uri("queue:send-invoice-to-peppol")
);
await endpoint.Send(new SendInvoiceToPeppol(invoiceId));
return Accepted(); // returns immediately
}
}
IMPORTANT:
-
Producer does NOT know RabbitMQ
-
Producer does NOT know exchanges or bindings
-
Producer sends a command
-
MassTransit handles routing
3. WHAT MASSTRANSIT DOES (AUTOMATICALLY)¶
Inside RabbitMQ (you don’t write this):
-
creates an exchange for
SendInvoiceToPeppol -
creates a queue
send-invoice-to-peppol -
binds exchange → queue
-
configures delivery semantics
RabbitMQ objects exist, but you don’t manage them manually.
4. CONSUMER (MESSAGE HANDOFF)¶
public class SendInvoiceToPeppolConsumer
: IConsumer<SendInvoiceToPeppol>
{
public async Task Consume(
ConsumeContext<SendInvoiceToPeppol> context)
{
BackgroundJob.Enqueue<PeppolJob>(
j => j.Execute(context.Message.InvoiceId)
);
// If this method returns successfully:
// - MassTransit ACKs the RabbitMQ message
await Task.CompletedTask;
}
}
RULES:
-
Consumer is thin
-
No Peppol logic here
-
No retries here
-
No HTTP calls here
This consumer’s only job:
handoff to Hangfire
5. HANGFIRE JOB (REAL WORK)¶
public class PeppolJob
{
[AutomaticRetry(Attempts = 5)]
public async Task Execute(Guid invoiceId)
{
// slow, unreliable work
var xml = GeneratePeppolXml(invoiceId);
await SendToPeppolAsync(xml);
}
}
-
Retries handled by Hangfire
-
Failures visible in dashboard
-
Job survives process crashes
RabbitMQ is not involved beyond delivery.
6. MASSTRANSIT CONFIGURATION¶
services.AddMassTransit(x =>
{
x.AddConsumer<SendInvoiceToPeppolConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("rabbitmq", "/", h =>
{
h.Username("guest");
h.Password("guest");
});
cfg.ReceiveEndpoint(
"send-invoice-to-peppol", e =>
{
e.ConfigureConsumer<
SendInvoiceToPeppolConsumer>(context);
});
});
});
This single block causes:
-
exchange creation
-
queue creation
-
binding creation
-
consumer wiring
7. END-TO-END FLOW (MECHANICAL)¶
-
HTTP request hits API
-
Invoice saved
-
API sends
SendInvoiceToPeppol -
MassTransit publishes to RabbitMQ
-
RabbitMQ delivers message
-
MassTransit consumer receives it
-
Consumer enqueues Hangfire job
-
Message is ACKed
-
Hangfire executes job
-
Peppol receives invoice
Delivery and execution are cleanly separated.
8. RETRY BOUNDARIES (CRITICAL)¶
-
RabbitMQ / MassTransit
→ delivery retry only (crashes, network issues) -
Hangfire
→ execution retry (Peppol down, timeouts)
They never overlap.
9. FINAL INVARIANTS¶
-
Producer publishes intent
-
MassTransit manages topology
-
Consumer hands off work
-
Hangfire executes work
-
RabbitMQ only moves messages
END CHECKPOINT