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.
Event | Description |
---|---|
ValidateOrder.Completed | The response was received |
ValidateOrder.TimeoutExpired | The request timed out |
ValidateOrder.Faulted | The 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:
Property | Type | Description |
---|---|---|
ServiceAddress | Uri? | If specified, the endpoint address of the request service. If unspecified, the request is published. |
Timeout | TimeSpan | The 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. |
TimeToLive | TimeSpan? | 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.
Property | Description |
---|---|
Completed | The first response event |
Completed2 | The second response event, if specified |
Completed3 | The third response event, if specified |
Faulted | The Fault<TRequest> event |
TimeoutExpired | The 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.
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();
});