MassTransit Skills Runbook

website·Indexed 33 days ago

https://masstransit.massient.com/

A collection of foundational guides and agent enforcement rules for building reliable distributed systems with MassTransit. Covers core patterns including Consumer Foundation, Saga State Machines, Routing Slip Activities, Request/Response flows, Outbox coordination, Endpoint Configuration, and the Test Harness.

Tokens
6.2K
Snippets
20
Records
31
Agent score
68%

What's inside MassTransit

  1. Implement Request/Response flows in MassTransit

    Implement request/response flows that are explicit about success, expected failures, and timeout behavior. Use record types for request and response contracts. Avoid using exceptions for expected business negatives (e.g., 'Not Found' results); instead, define multiple response types to represent different business outcomes.
  2. Implement the MassTransit Agent Workflow

    When using the Skills Runbook to implement a feature, follow this sequence:

    1. Select Skill: Choose one primary skill guide based on the task (e.g., Consumer Foundation).
    2. Apply Guidance: Implement the feature while preserving existing application patterns.
    3. Cross-Reference: Borrow compatible patterns from secondary skills only if necessary.
    4. Verify: Execute the Verification commands specified within the selected skill guide.
    5. Align: Confirm the generated code matches the linked concepts, configuration, and guides.
  3. Implement a basic MassTransit Saga State Machine

    To build a saga state machine that handles long-running workflows, you need a state class inheriting from SagaStateMachineInstance and a state machine class inheriting from MassTransitStateMachine<TState>.

    Key requirements include:

    • Defining CorrelationId and CurrentState in the state class.
    • Explicitly correlating every event using CorrelateById.
    • Modeling deterministic transitions (Start, Progress, Complete, Fail, Timeout).
    • Using record types for message contracts to ensure consistency.
    public record SubmitOrder(Guid OrderId);
    public record OrderAccepted(Guid OrderId);
    
    public class OrderState : SagaStateMachineInstance
    {
        public Guid CorrelationId { get; set; }
        public string CurrentState { get; set; } = default!;
        public DateTime? SubmittedAt { get; set; }
    }
    
    public class OrderStateMachine : MassTransitStateMachine<OrderState>
    {
        public State Submitted { get; private set; } = default!;
        public State Accepted { get; private set; } = default!;
    
        public Event<SubmitOrder> SubmitOrder { get; private set; } = default!;
        public Event<OrderAccepted> OrderAccepted { get; private set; } = default!;
    
        public OrderStateMachine()
        {
            InstanceState(x => x.CurrentState);
    
            Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
            Event(() => OrderAccepted, x => x.CorrelateById(context => context.Message.OrderId));
    
            Initially(
                When(SubmitOrder)
                    .Then(context => context.Saga.SubmittedAt = InVar.Timestamp)
                    .TransitionTo(Submitted));
    
            During(Submitted,
                When(OrderAccepted)
                    .TransitionTo(Accepted)
                    .Finalize(),
                Ignore(SubmitOrder));
    
            SetCompletedWhenFinalized();
        }
    }
  4. Best practices for building MassTransit consumers

    Follow these guardrails to ensure consumers remain simple, reliable, and testable:

    Do:

    • Use record types for message contracts to ensure stable, explicit fields.
    • Scope each consumer to a single business responsibility.
    • Use delayed redelivery for recovery that may take longer than a standard retry.

    Do Not:

    • Do not combine unrelated workflows in a single consumer.
    • Do not use sync-over-async code (avoid blocking threads).
    • Do not swallow exceptions; preserve the failure context to allow MassTransit to handle retries/faults.
    • Do not publish side-effect events before durable state changes have occurred.
  5. Define consumer-specific behavior using ConsumerDefinition

    Use a ConsumerDefinition to encapsulate retry policies, outbox configuration, and concurrency limits for a specific consumer. Note that a ConcurrentMessageLimit set within a consumer definition limits only that specific consumer, not every consumer sharing the endpoint.
    public class SubmitOrderConsumerDefinition : ConsumerDefinition<SubmitOrderConsumer>
    {
        public SubmitOrderConsumerDefinition()
        {
            EndpointName = "order-service";
            ConcurrentMessageLimit = 8;
        }
    
        protected override void ConfigureConsumer(
            IReceiveEndpointConfigurator endpointConfigurator,
            IConsumerConfigurator<SubmitOrderConsumer> consumerConfigurator,
            IRegistrationContext context)
        {
            endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 200, 500, 800, 1000));
            endpointConfigurator.UseInMemoryOutbox(context);
        }
    }
  6. Follow MassTransit global development rules

    Adhere to these global standards when generating MassTransit code:

    • Message Contracts: Prefer record types for new application-owned contracts. Use classes or interfaces only for interoperability or existing contract requirements.
    • Behavior: Ensure consumer, saga, and activity behavior is deterministic and explicit.
    • Configuration: Prefer registration and definitions over ad-hoc endpoint code.
    • Transport: Keep transport assumptions explicit.
    • Error Handling: Do not rely on exceptions for expected business outcomes in request/response flows.
    • Outbox Usage: Do not publish events before durable updates when an outbox is required. Refer to the outbox selection guide to choose the correct type.
  7. Set up and use MassTransit Test Harness for deterministic testing

    The MassTransit Test Harness allows for deterministic testing of consumers, sagas, and request/response behavior. To ensure test reliability, start one harness per scenario and avoid reusing harnesses across unrelated tests. Use async harness collections (Consumed, Sent, Published) for assertions and targeted consumer or saga harnesses for behavior-level validation.
    [Test]
    public async Task Submit_order_should_be_consumed_and_publish_event()
    {
        await using var provider = new ServiceCollection()
            .AddMassTransitTestHarness(x =>
            {
                x.AddConsumer<SubmitOrderConsumer>();
            })
            .BuildServiceProvider(true);
    
        var harness = await provider.StartTestHarness();
    
        await harness.Bus.Publish(new SubmitOrder(InVar.Id, "123"));
    
        // Assert transport-level behavior
        Assert.That(await harness.Consumed.Any<SubmitOrder>(), Is.True);
        Assert.That(await harness.Published.Any<OrderSubmitted>(), Is.True);
    
        // Assert specific consumer behavior
        var consumerHarness = harness.GetConsumerHarness<SubmitOrderConsumer>();
        Assert.That(await consumerHarness.Consumed.Any<SubmitOrder>(), Is.True);
    }
  8. Configure MassTransit endpoints using convention-based setup

    Use ConfigureEndpoints(context) to automatically set up endpoints based on registered consumers. To separate environments on shared brokers (like SQS), use SetEndpointNameFormatter with a prefix to ensure unique queue naming across environments.
    services.AddMassTransit(x =>
    {
        x.AddConsumer<SubmitOrderConsumer>();
        
        // Use a prefix (e.g., "dev") to separate environments on shared brokers
        x.SetEndpointNameFormatter(new KebabCaseEndpointNameFormatter(prefix: "dev"));
    
        x.UsingRabbitMq((context, cfg) =>
        {
            cfg.ConfigureEndpoints(context);
        });
    });
  9. Schedule timeouts in a Saga State Machine

    To handle timeouts in a saga, define a Schedule object in the state machine and a corresponding token property in the state instance. You must configure a transport message scheduler or scheduler integration before using this functionality. Use .Schedule() during a transition to initiate the timer and .Received to handle the timeout event.
    public record OrderCompletionTimeoutExpired(Guid OrderId);
    
    public class OrderStateWithTimeout : SagaStateMachineInstance
    {
        public Guid CorrelationId { get; set; }
        public string CurrentState { get; set; } = default!;
        public Guid? OrderCompletionTimeoutTokenId { get; set; }
    }
    
    public class OrderStateMachineWithTimeout : MassTransitStateMachine<OrderStateWithTimeout>
    {
        public Schedule<OrderStateWithTimeout, OrderCompletionTimeoutExpired> OrderCompletionTimeout { get; private set; } = default!;
        public Event<SubmitOrder> SubmitOrder { get; private set; } = default!;
    
        public OrderStateMachineWithTimeout()
        {
            Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
            
            Schedule(() => OrderCompletionTimeout, x => x.OrderCompletionTimeoutTokenId, s =>
            {
                s.Delay = TimeSpan.FromDays(30);
                s.Received = r => r.CorrelateById(context => context.Message.OrderId);
            });
    
            Initially(When(SubmitOrder)
                .Schedule(OrderCompletionTimeout, context => context.Init<OrderCompletionTimeoutExpired>(context.Message)));
    
            DuringAny(When(OrderCompletionTimeout.Received).Finalize());
            SetCompletedWhenFinalized();
        }
    }
  10. Implement Outbox patterns for message consistency

    The Outbox pattern coordinates outgoing messages with successful business work to ensure end-to-end consistency. Depending on the durability requirements, choose one of the following implementations:

    • In-Memory Outbox: Buffers messages produced by a consumer until the current attempt succeeds. It is NOT durable across process failures.
    • Transactional Consumer Outbox: Used when consumer state, duplicate detection, and outgoing messages must be stored together in a single transaction.
    • Transactional Bus Outbox: Used when an API or application scope writes state and publishes/sends messages together.

    Core Rules:

    • Keep handlers idempotent to ensure safety during replays.
    • Ensure persistence updates are durable before triggering publish/send side effects.
    • Use retry or redelivery only for transient failures, not for business failures that cannot succeed on a subsequent attempt.
  11. Implement a Routing Slip Activity with Compensation

    To build a routing slip activity with reliable compensation, implement the IActivity<TArguments, TLog> interface. The activity should validate arguments before performing side effects in the Execute method and provide a deterministic Compensate method to reverse those side effects using data stored in the compensation log (TLog).

    Key Guidelines:

    • Use record types for arguments and compensation logs.
    • Validate all arguments at the start of Execute before any side effects occur.
    • Ensure the compensation log contains all necessary data to reverse the action.
    • Model irreversible side effects explicitly rather than attempting to compensate them.
    public record DownloadImageArguments(string ImageUri, string WorkPath);
    
    public record DownloadImageLog(string ImageSavePath);
    
    public class DownloadImageActivity : IActivity<DownloadImageArguments, DownloadImageLog>
    {
        public async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> context)
        {
            // 1. Validate arguments before side effects
            if (!Uri.TryCreate(context.Arguments.ImageUri, UriKind.Absolute, out _))
                throw new ArgumentException("ImageUri must be an absolute URI", nameof(context.Arguments.ImageUri));
    
            if (string.IsNullOrWhiteSpace(context.Arguments.WorkPath))
                throw new ArgumentException("WorkPath is required", nameof(context.Arguments.WorkPath));
    
            // 2. Perform side effect
            Directory.CreateDirectory(context.Arguments.WorkPath);
            var filePath = Path.Combine(context.Arguments.WorkPath, context.TrackingNumber.ToString());
            await File.WriteAllTextAsync(filePath, "downloaded", context.CancellationToken);
    
            // 3. Return completion with log data needed for compensation
            return context.Completed(new DownloadImageLog(filePath));
        }
    
        public Task<CompensationResult> Compensate(CompensateContext<DownloadImageLog> context)
        {
            // 4. Use the log to reverse the side effect
            File.Delete(context.Log.ImageSavePath);
            return Task.FromResult(context.Compensated());
        }
    }
  12. Configure manual receive endpoints with explicit limits

    Use ReceiveEndpoint(...) when precise queue-level control is required. You can set the global PrefetchCount on the transport configuration and a specific ConcurrentMessageLimit on the endpoint to control throughput and ensure downstream stability.
    services.AddMassTransit(x =>
    {
        x.AddConsumer<SubmitOrderConsumer>();
    
        x.UsingRabbitMq((context, cfg) =>
        {
            cfg.PrefetchCount = 32;
    
            cfg.ReceiveEndpoint("order-service", e =>
            {
                e.ConcurrentMessageLimit = 28;
                e.ConfigureConsumer<SubmitOrderConsumer>(context);
            });
        });
    });