State Machine Requests

Request/response is easily the most commonly used integration pattern. A service sends a request to another service and continues after receiving the response. Most of the time, waiting for the response is a blocking operation โ€“ the requester waits for the response before it continues processing. In early days of software development, blocking could limit overall system throughput. However, with modern async/await solutions and the .NET Task Parallel Library (TPL), the impact of waiting is mitigated.

In event-based application, the combination of a command followed by an event usually refines down to the same request/response pattern. In many cases, the event produced is only interesting to the command's sender.

Saga state machines support request/response, both as a requester and a responder. Unlike the request client, however, support for requests is asynchronous at the message-level, eliminating the overhead of waiting for the response. After the request is produced, the saga state machine instance is persisted. When the response is received, the instance is loaded and the response is consumed by the state machine.

Declaring Requests

Requests are declared as public properties on the saga state machine with the Request<TSaga, TRequest, TResponse> property type where TSaga is the saga state machine instance type and both TRequest and TResponse are valid message types.

public record ValidateOrder(Guid OrderId);
public record OrderValidated(Guid OrderId);

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public Request<OrderState, ValidateOrder, OrderValidated> 
        ValidateOrder { get; private set; } = null!;
    
    public OrderStateMachine()
    {
        Request(() => ValidateOrder, o =>
        {
            o.Timeout = TimeSpan.FromMinutes(30);
        });
    }
}

In the example above, ValidateOrder is a request to an order validation service that responds with OrderValidated. One of three possible outcomes will happen after the request is produced.

EventDescription
ValidateOrder.CompletedThe response was received
ValidateOrder.TimeoutExpiredThe request timed out
ValidateOrder.FaultedThe order validation service faulted

The request also includes a ValidateOrder.Pending state that can optionally be used while the request is pending.

Request Configuration

The request options can be configured using the configuration callback. In the example above, the Timeout option is set. The complete list of request options includes:

PropertyTypeDescription
ServiceAddressUri?If specified, the endpoint address of the request service. If unspecified, the request is published.
TimeoutTimeSpanThe request timeout. If set to TimeSpan.Zero, the request never times out. This is useful for requests that are guaranteed to complete or fault and reduces the load on the message scheduler.
TimeToLiveTimeSpan?The request message time-to-live, which is used by the transport to automatically delete the message after the time period elapses. If unspecified, and the Timeout is greater than TimeSpan.Zero, the timeout value is used.

Response Types

Requests usually have a single response, however, up to two additional responses are supported. Additional response types are specified as generic parameters on the request property.

In the example below, the request includes an additional response type OrderNotValid.

public record ValidateOrder(Guid OrderId);
public record OrderValidated(Guid OrderId);
public record OrderNotValid(Guid OrderId);

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public Request<OrderState, ValidateOrder, OrderValidated, OrderNotValid> 
        ValidateOrder { get; private set; } = null!;
    
    public OrderStateMachine()
    {
        Request(() => ValidateOrder, o =>
        {
            o.Timeout = TimeSpan.FromMinutes(30);
        });
    }
}

Response Events

Additionally, each request event can be configured allowing complete control over how the response is correlated to the saga state machine instance.

PropertyDescription
CompletedThe first response event
Completed2The second response event, if specified
Completed3The third response event, if specified
FaultedThe Fault<TRequest> event
TimeoutExpiredThe timeout event

This can be useful to configure how an event is configured on the message broker. For example, to remove the response type bindings from the message broker, the events can be configured with ConfigureConsumeTopology = false. Since responses are always sent to the ResponseAddress specified by the requester, the bindings are not necessary and can be eliminated.

public record ValidateOrder(Guid OrderId);
public record OrderValidated(Guid OrderId);

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public Request<OrderState, ValidateOrder, OrderValidated> 
        ValidateOrder { get; private set; } = null!;
    
    public OrderStateMachine()
    {
        Request(() => ValidateOrder, r =>
        {
            r.Timeout = TimeSpan.FromMinutes(30);
            
            r.Completed = e => e.ConfigureConsumeTopology = false;
            r.Faulted = e => e.ConfigureConsumeTopology = false;
            r.TimeoutExpired = e => e.ConfigureConsumeTopology = false;
        });
    }
}

Sending Requests

To send a request, add a Request activity to an event behavior as shown in the example below.

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public Request<OrderState, ValidateOrder, OrderValidated> 
        ValidateOrder { get; private set; } = null!;

    public OrderStateMachine()
    {
        Initially(
            When(OrderSubmitted)
                .Request(ValidateOrder,
                    x => new ValidateOrder(x.Saga.CorrelationId))
                .TransitionTo(ValidateOrder.Pending)
        );
    }
}

The request is published with the RequestId set the saga state machine instance CorrelationId (since no RequestId property was specified) and the ResponseAddress set to the receive endpoint address of the saga state machine.

In this example, the ValidateOrder.Pending state is used while the request is pending. However, any state defined in the saga state machine can be used.

Handling Responses

When the response is received, the Completed event is triggered. If the order validation service threw an exception, the Faulted event is triggered instead.

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public Request<OrderState, ValidateOrder, OrderValidated> 
        ValidateOrder { get; private set; } = null!;

    public OrderStateMachine()
    {
        During(ValidateOrder.Pending,
            // Handle the valid response
            When(ValidateOrder.Completed)
                .TransitionTo(Completed),

            // Handle a validation fault
            When(ValidateOrder.Faulted)
                .TransitionTo(Failed)
        );
    }
}

Request Overrides

There are many different Request method overrides that can be used depending on the features required. A few examples are shown below.

Service Address

Specify the service address for the request, optionally using the contents of the saga state machine instance or the event (via context.Message). Useful when the instance stores data about which service should process the request.

.Request(ValidateOrder, serviceAddress, 
    context => new ValidateOrder(context.Saga.CorrelationId))

.Request(ValidateOrder, context => context.Saga.ServiceAddress, 
    context => new ValidateOrder(context.Saga.CorrelationId))

Async Message Factory

The request message can be created asynchronously, if a message initializer is used or when the request message needs data returned by an asynchronous method.

.Request(ValidateOrder,  
    async context => new ValidateOrder(context.Saga.CorrelationId))

.Request(ValidateOrder, context => context.Saga.ServiceAddress, 
    async context => new ValidateOrder(context.Saga.CorrelationId))
    
.Request(ValidateOrder, async context => 
{
    await Task.Delay(1); // some async method 
    return new ValidateOrder();
});