# 7.1.0
NOTE
MassTransit 7.1.0 is a minor version update. There are a couple of minor interface changes, such as IRequestClient<T>
detailed below. Any required changes after updating the package references should be minor.
View the Pull Request for a list of all commits (opens new window)
# Re(Start) the Bus – Finally!
Since MassTransit's inception, it has never been possible to Start a bus that was previously Started and then Stopped. With this release, a bus can now be started, stopped, started, and stopped, and started, and stopped...
It’s like removing a doorstop you’ve tripped over for years.
Seriously, this is a big deal. Since containers are mainstream at this point along with having the most excellent .AddMassTransit
configuration experience, the ability to stop the bus (temporarily, or whatever) without having to configure a new bus instance from scratch is huge. There are a lot of ideas brewing that take advantage of this new capability, so stay tuned!
# Request Client
There have been a couple of updates to the request client to improve usability and interoperability.
# Request Client Multiple Response Types
The method signature for the GetResponse<T1,T2>(...)
method has been changed. The previous method signature returned a tuple, as shown below:
Task<(Task<Response<T1>>, Task<Response<T2>>)> GetResponse<T1, T2>(TRequest message, CancellationToken cancellationToken, RequestTimeout timeout)
The new signature uses a new type, Response<T1,T2>
, which is a readonly struct that can be used to more easily identify which response was received.
Task<Response<T1, T2>> GetResponse<T1, T2>(TRequest message, CancellationToken cancellationToken, RequestTimeout timeout)
Using the new return type, handling multiple responses is now cleaner:
var response = await client.GetResponse<ResponseA, ResponseB>(new Request());
if (response.Is(out Response<ResponseA> responseA))
{
// do something with responseA
}
else if (response.Is(out Response<ResponseB> responseB))
{
// do something with responseB
}
As always, if the request times out, or if a Fault<Request>
is produced by the consumer, the initial await will throw a RequestTimeoutException
or RequestFaultException
respectively. The signature change only introduces a new return type.
To retain backwards compatibility, a Deconstruct method is available to access the two response tasks. The side effect of this choice is that there can only be a single Deconstruct method with two arguments. So, a creative choice was made to provide a pattern-matching solution as well.
By specifying the explicit type, Response
for the return value, modern C# pattern can be used via deconstruction.
Response response = await client.GetResponse<ResponseA, ResponseB>(new Request());
// Using a regular switch statement
switch (response)
{
case (_, ResponseA a) responseA:
// responseA in the house
break;
case (_, ResponseB b) responseB:
// responseB if it isn't A
break;
default:
// wow, we really should NOT get here
break;
}
// Or using a switch expression
var accepted = response switch
{
(_, ResponseA a) => true,
(_, ResponseB b) => false,
_ => throw new InvalidOperationException()
};
The first tuple element is the Response
type, which includes MessageContext
so that the message headers can be examined. In the example above, it is discarded since the response
variable is already in scope.
# Request Client Accept Response Types
Another change to the request client is the addition of a new message header, MT-Request-AcceptType
, which is set by the request client and contains the message types that have been specified by the request client. This allows the request consumer to determine if the client can handle a response type, which can be useful as services evolve and new response types may be added to handle new conditions. For instance, if a consumer adds a new response type, such as OrderAlreadyShipped
, if the response type isn't supported an exception may be thrown instead.
To see this in code, check out the client code:
var response = await client.GetResponse<OrderCanceled, OrderNotFound>(new CancelOrder());
if (response.Is(out Response<OrderCanceled> canceled))
{
return Ok();
}
else if (response.Is(out Response<OrderNotFound> responseB))
{
return NotFound();
}
The original consumer, prior to adding the new response type:
public async Task Consume(ConsumeContext<CancelOrder> context)
{
var order = _repository.Load(context.Message.OrderId);
if(order == null)
{
await context.ResponseAsync<OrderNotFound>(new { context.Message.OrderId });
return;
}
order.Cancel();
await context.RespondAsync<OrderCanceled>(new { context.Message.OrderId });
}
Now, the new consumer that checks if the order has already shipped:
public async Task Consume(ConsumeContext<CancelOrder> context)
{
var order = _repository.Load(context.Message.OrderId);
if(order == null)
{
await context.ResponseAsync<OrderNotFound>(new { context.Message.OrderId });
return;
}
if(order.HasShipped)
{
if (context.IsResponseAccepted<OrderAlreadyShipped>())
{
await context.RespondAsync<OrderAlreadyShipped>(new { context.Message.OrderId, order.ShipDate });
return;
}
else
throw new InvalidOperationException("The order has already shipped"); // to throw a RequestFaultException in the client
}
order.Cancel();
await context.RespondAsync<OrderCanceled>(new { context.Message.OrderId });
}
This way, the consumer can check the request client response types and act accordingly.
NOTE
For backwards compatibility, if the new MT-Request-AcceptType
header is not found, IsResponseAccepted
will return true for all message types.
# In-Memory Outbox Configuration
There were some unexpected changes in 7.0.7 related to how the In-Memory Outbox gets added to the consume pipeline for batch consumers. This was changed again to ensure that only one outbox is created for the batch consumer and that it is added prior to the consumer factory so that resolving consumer dependencies get the proper ConsumeContext
(the outbox one).
# Scoped Consume Filters
The scoped consume filter was updated to ensure the scoped filter is resolved from the container after the consumer/saga scope is created.
# Receive Endpoint Connector
When using container integration, it is now possible to connect receive endpoints using previously added consumer types. To connect a receive endpoint using MS DI, consider the following example.
var connector = provider.GetRequiredService<IReceiveEndpointConnector>();
var handle = connector.ConnectReceiveEndpoint("some-queue", (context,cfg) => cfg.ConfigureConsumer<SomeConsumer>(context));
await handle.Ready;
The
ConfigureEndpoints
method has an optional filter that can be used to excludeSomeConsumer
from having a receive endpoint created.