Skip to content

Instantly share code, notes, and snippets.

@idusortus
Last active September 7, 2025 17:44
Show Gist options
  • Save idusortus/fe6dd09fb7fbe2c7fe6b82e4e560328e to your computer and use it in GitHub Desktop.
Save idusortus/fe6dd09fb7fbe2c7fe6b82e4e560328e to your computer and use it in GitHub Desktop.

πŸš€ A Developer's Guide to Messaging in .NET

This document covers building robust, scalable, and resilient applications using messaging patterns in the .NET ecosystem. Touches upon foundational concepts and advanced, real-world implementation and operational strategies.


🧠 1. The "Why": Core Messaging Concepts

The fundamental problem that messaging solves is decoupling.

  • Asynchronous Communication: Allows services to communicate without waiting for a direct, synchronous response. This improves responsiveness and user experience.
  • Resilience & Fault Tolerance: If a consumer service is down, messages can wait safely in a queue until the service is available again, preventing data loss.
  • Scalability & Load Leveling: A message queue acts as a buffer, absorbing spikes in traffic. You can scale the number of consumers (workers) independently of the publisher (API).
  • Service Autonomy: The message publisher doesn't need to know who is listening. New services can subscribe to events without requiring any changes to the original publisher.

🏒 2. In-Process vs. Distributed Messaging

We identified two distinct levels of messaging, each with its own primary tool.

Feature MediatR (In-Process) πŸ“¦ Azure Service Bus / RabbitMQ (Distributed) 🌐
Scope Within a single application process. Between different services, across a network.
Use Case Clean internal architecture, CQRS. Asynchronous background jobs, microservices communication.
Persistence None. Messages are lost on app restart. Yes. Messages are durably stored in a message broker.
Key Pattern IRequest (Command/Query), INotification (Pub/Sub) Competing Consumers (Queues), Pub/Sub (Topics)

πŸ’‘ Key Insight: They are not competitors; they are complementary. Use MediatR for a clean internal architecture and a durable broker like Azure Service Bus for resilient inter-service communication.


broker 3. Choosing a Message Broker: Azure Service Bus vs. RabbitMQ

When you need a durable, out-of-process broker, two main options stand out.

  • Azure Service Bus (ASB) ☁️

    • Type: Fully managed PaaS (Platform as a Service).
    • Pros: βœ… Zero management overhead, seamless Azure integration (identity, monitoring), built-in disaster recovery.
    • Cons: ❌ Vendor lock-in, less routing flexibility than RabbitMQ.
    • Best For: Teams building Azure-native applications who want to focus on business logic, not infrastructure management.
  • RabbitMQ πŸ‡

    • Type: Open-source message broker software (self-hosted or managed).
    • Pros: βœ… Runs anywhere (multi-cloud/on-prem), extremely flexible routing via exchanges, powerful management UI.
    • Cons: ❌ You are responsible for hosting, clustering, patching, and monitoring (high operational overhead).
    • Best For: Multi-cloud environments, complex routing needs, or teams with existing operational expertise.

πŸ› οΈ 4. Choosing an Abstraction Framework: NServiceBus vs. MassTransit

These frameworks sit on top of a broker to enforce patterns and dramatically improve developer productivity.

  • NServiceBus πŸ’Ό

    • Philosophy: Commercial, opinionated, and prescriptive. Creates a "pit of success."
    • Pros: βœ… World-class commercial tooling (ServiceInsight, Pulse), professional support with an SLA.
    • Cons: ❌ Requires a paid license, less flexible than MassTransit.
  • MassTransit 🚌

    • Philosophy: Free and open-source (FOSS), flexible, and powerful. A comprehensive toolkit.
    • Pros: βœ… No cost, extremely powerful State Machine Sagas, ultimate flexibility.
    • Cons: ❌ Community-only support, requires you to "bring your own" observability stack (OpenTelemetry, Grafana, etc.).

βš™οΈ 5. Hands-On Implementation: Competing Consumers with MassTransit & RabbitMQ

We built a complete solution to demonstrate the Competing Consumers pattern. This involved a Web API publisher and a Worker Service with two consumers.

🚨 The Troubleshooting Journey - Key Learnings:

  1. Problem: bus.Send() fails with A convention for the message type... was not found.

    • πŸ’‘ Solution: The publisher must be told where to send commands. This is done by configuring a global, static mapping: EndpointConvention.Map<MyCommand>(new Uri("queue:my-queue-name"));
  2. Problem: Messages are sent but consumers don't receive them.

    • πŸ’‘ Solution: The consumer's endpoint name must match the publisher's convention. We configured the consumer explicitly: cfg.ReceiveEndpoint("my-queue-name", e => ...);
  3. Problem: Both consumers received a copy of every message, even when using bus.Send().

    • πŸ’‘ Solution: This was a subtle issue caused by a stale broker state from previous test runs. The key to developing with messaging is to always ensure a clean broker state. Delete application-specific queues and exchanges in the RabbitMQ UI before starting a fresh debugging session. The modern MassTransit v8+ default topology (a fanout exchange bound to a queue of the same name) correctly implements competing consumers.

πŸ—ΊοΈ 6. Advanced Workflow Orchestration: The Saga Pattern

For long-running, multi-step processes (like a file ingestion pipeline), we discussed two primary approaches:

  • Saga (Orchestration) πŸ§‘β€βœˆοΈ:

    • A central "orchestrator" state machine that sends commands to workers and reacts to their events.
    • Pros: High visibility of the workflow logic, centralized error handling.
    • Recommended For: Well-defined, business-critical processes.
  • Routing Slip (Choreography) πŸ“œ:

    • An "itinerary" is attached to the message itself, which self-routes from one worker to the next.
    • Pros: Highly decoupled; no central point of failure.
    • Cons: Poor visibility into the overall state of the process.

🚦 7. Dynamic Saga Control: Pausing & Skipping Steps

We explored strategies for modifying a live Saga workflow without downtime, using a Feature Flag system (like Azure App Configuration) as the recommended approach.

  • To Pause/Suspend a Step:

    1. The Saga checks a flag (IsStepXEnabled).
    2. If false, the Saga transitions to a dedicated StepXSuspended state instead of sending the next command.
    3. A separate process is needed to "wake up" suspended sagas once the flag is re-enabled.
  • To Skip a Step Entirely:

    1. The Saga checks a flag (IsStepXSkipped).
    2. If true, the Saga logs the skip for auditing.
    3. It then immediately sends the command for the following step (e.g., sends the Step 4 command instead of the Step 3 command) and transitions directly to the following state.
    4. 🚨 Caution: Downstream consumers must be resilient to potentially missing data from the skipped step.

Of course. Here is the summary of our discussion on authentication and authorization, formatted and ready to be appended as point #8 to the Markdown document.


πŸ›‘οΈ 8. Authentication & Authorization in a Distributed System

Handling user identity in an asynchronous, distributed messaging system is a critical security challenge. Passing JWTs in messages is an anti-pattern that introduces significant security, performance, and reliability issues.

🚨 The Anti-Pattern: Embedding JWTs in Messages

  1. Performance πŸ“‰: JWTs add significant size (1-2KB+) to every message, increasing network, storage, and CPU overhead at scale.
  2. Security πŸ›‘οΈ: A JWT is a bearer token. If a message is ever compromised, the token can be stolen and used in replay attacks to impersonate the user.
  3. Reliability ⏳: JWTs have short expiry times. For any long-running or delayed process, the token will be expired by the time the consumer processes the message, breaking the workflow.

βœ… The Solution: The Trusted Subsystem Model

The correct approach is to shift from authenticating the user at every step to authenticating the service at the infrastructure level.

⭐ Pattern 1: Infrastructure-Level Security

Secure the "pipes" between your services and the message broker. The broker acts as the bouncer, ensuring only trusted applications can connect.

  • How: Use strong, infrastructure-native authentication mechanisms.
    • With Azure Service Bus: Use Managed Identity for passwordless, secret-free authentication between your Azure-hosted services and the broker.
    • With RabbitMQ: Use TLS with client certificates (mTLS) or securely managed credentials.
  • Outcome: Any consumer connected to the broker is now considered part of a trusted subsystem.

⭐ Pattern 2: Passing Identity, Not Tokens

Once a service is trusted, it doesn't need proof of identity (the JWT); it just needs the identity's essential details.

  • How: The API, as the public gateway, is the only service that validates the inbound JWT. After validation, it extracts the necessary, non-sensitive claims and copies them into the message.

  • The Message Contract: The message should carry the who and what, not the proof.

    // The JWT is NOT here. Only the necessary, validated data is.
    public record SubmitOrder(
        Guid OrderId,
        string CustomerNumber,
        
        // --- Identity Context ---
        Guid UserId,       // The 'sub' claim from the JWT
        Guid TenantId,     // The 'tenant_id' claim
        string CorrelationId // For end-to-end tracing
    );
  • Outcome: The consumer implicitly trusts the UserId and TenantId in the message because it came over a secure channel from a trusted publisher. This completely solves the token expiry problem for long-running processes like Sagas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment