Skip to content

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)

  1. HTTP request hits API

  2. Invoice saved

  3. API sends SendInvoiceToPeppol

  4. MassTransit publishes to RabbitMQ

  5. RabbitMQ delivers message

  6. MassTransit consumer receives it

  7. Consumer enqueues Hangfire job

  8. Message is ACKed

  9. Hangfire executes job

  10. 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