# 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 exclude SomeConsumer from having a receive endpoint created.