Unit of Work

Component: NServiceBus
NuGet Package NServiceBus (6.x)

Using a transaction scope

If a business transaction is spread across multiple handlers there is always a risk of partial updates since one handler might succeed in updating the data while other won't. To avoid this use a unit of work that wraps all handlers in a TransactionScope and makes sure that there are no partial updates. Use following code to enable a wrapping scope:

var unitOfWork = endpointConfiguration.UnitOfWork();
unitOfWork.WrapHandlersInATransactionScope();
This requires the selected persistence to support enlisting in transaction scopes.
This might escalate to a distributed transaction if data in different databases are updated.
This API must not be used in combination with transports running in transaction scope mode. Wrapping handlers in a TransactionScope in such a situation throws an exception.

Controlling transaction scope options

The following options for transaction scopes used to wrap all handlers can be configured.

Isolation level

NServiceBus will by default use the ReadCommitted isolation level.

Change the isolation level using

var unitOfWork = endpointConfiguration.UnitOfWork();
unitOfWork.WrapHandlersInATransactionScope(
    isolationLevel: IsolationLevel.RepeatableRead);

Transaction timeout

NServiceBus will use the default transaction timeout of the machine the endpoint is running on.

Change the transaction timeout using

var unitOfWork = endpointConfiguration.UnitOfWork();
unitOfWork.WrapHandlersInATransactionScope(
    timeout: TimeSpan.FromSeconds(30));

Or via .config file using a example DefaultSettingsSection.

Implementing custom unit of work

A unit of work allows for shared code, that wraps handlers, to be reused in a way that doesn't pollute the individual handler code. For example, committing NHibernate transactions, or calling SaveChanges on a RavenDB session.

IManageUnitsOfWork

To create a unit of work, implement the IManageUnitsOfWork interface.

public class MyUnitOfWork :
    IManageUnitsOfWork
{
    public Task Begin()
    {
        // Do custom work here
        return Task.CompletedTask;
    }

    public Task End(Exception ex = null)
    {
        // Do custom work here
        return Task.CompletedTask;
    }
}

The semantics are that Begin() is called when the transport messages enters the pipeline. A transport message can consist of multiple application messages. This allows any setup that is required.

The End() method is called when the processing is complete. If there is an exception, it is passed into the method.

This gives a way to perform different actions depending on the outcome of the message(s).

In Versions 6 and above IManageUnitsOfWorks does not have access to the incoming message context. If access to headers and/or message body is required instead implement a unit of work using behaviors instead.
In Version 6 IManageUnitsOfWork does not wrap the persistence transaction. The persistence transaction can therefore still fail after the unit of work completed.
In NServiceBus Versions 6 and above, and all integrations that target those versions, all extension points that return Task cannot return a null Task. These APIs must return an instance of a Task. For example either a pending Task, a CompletedTask, or be marked async. For extension points that return a Task<T> return the value directly (for async methods) or wrap the value in a Task.FromResult(value).

Registering custom unit of work

After implementing a IManageUnitsOfWork, it needs to be registered:

endpointConfiguration.RegisterComponents(
    registration: components =>
    {
        components.ConfigureComponent<MyUnitOfWork>(DependencyLifecycle.InstancePerUnitOfWork);
    });

Samples


Last modified