Skip to content

Create a routing slip activity

A routing slip activity is a class that implements a processing step that can be added to a routing slip. An activity can be either a compensating activity or a non-compensating activity.

To create an activity, create a class that implements the IActivity<TArguments, TLog> interface for activities that support compensation or IExecuteActivity<TArguments> for those that don’t need compensation.

public class DownloadImageActivity :
IActivity<DownloadImageArguments, DownloadImageLog>
{
Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> context);
Task<CompensationResult> Compensate(CompensateContext<DownloadImageLog> context);
}

The IActivity<TArguments, TLog> interface has two generic arguments. The first specifies the activity’s argument type and the second specifies the activity’s log type. In the example above, DownloadImageArguments is the argument type and DownloadImageLog is the log type. The type parameters may be an interface, class, or record type. Where the type is a class or a record, the proper accessors should be specified (i.e. { get; set; } or { get; init; }).

public class DownloadImageActivity :
IExecuteActivity<DownloadImageArguments>
{
Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> context);
}
VerbDescription
ExecuteThe primary action that the activity performs as part of the workflow.
CompensateThe action the activity must take to undo or reverse its effects if any subsequent activities fail.

Both IActivityand IExecuteActivity require you to implement the Execute method. Execute is called while the routing slip is executing activities.

When Execute is called, the ExecuteContext argument contains the activity arguments, the routing slip’s TrackingNumber, and methods to complete or fault the activity. The actual routing slip message, as well as any details of the underlying infrastructure, are excluded to prevent coupling between the activity and the implementation. An example Execute method is shown below.

async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
DownloadImageArguments args = execution.Arguments;
string imageSavePath = Path.Combine(args.WorkPath,
execution.TrackingNumber.ToString());
await _httpClient.GetAndSave(args.ImageUri, imageSavePath);
return execution.Completed<DownloadImageLog>(new {ImageSavePath = imageSavePath});
}

After an activity finishes processing, it returns an ExecutionResult to the host. If the activity completes successfully, it can choose to store compensation data in an activity log, which is passed to the Completed method on the ExecuteContext argument. If no compensation data is needed, the activity log is optional. In addition to compensation data, the activity can also add or modify variables stored in the routing slip for use by subsequent activities.

ResultDescription
CompleteIndicates the activity completed successfully.
FaultIndicates the activity failed.
TerminateIndicates the routing slip should stop, but the process is considered complete.
async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
DownloadImageArguments args = execution.Arguments;
string imageSavePath = Path.Combine(args.WorkPath,
execution.TrackingNumber.ToString());
await _httpClient.GetAndSave(args.ImageUri, imageSavePath);
// success with no compensation log
return execution.Completed();
}
async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
DownloadImageArguments args = execution.Arguments;
string imageSavePath = Path.Combine(args.WorkPath,
execution.TrackingNumber.ToString());
await _httpClient.GetAndSave(args.ImageUri, imageSavePath);
return execution.Completed<DownloadImageLog>(new {ImageSavePath = imageSavePath});
}

In the example above, the activity specifies the DownloadImageLog interface and initializes the log using an anonymous object. The object is then passed to the Completed method for storage in the routing slip before sending the routing slip to the next activity.

An activity has complete control over the routing slip and can revise the itinerary to include additional activities.

async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
DownloadImageArguments args = execution.Arguments;
string imageSavePath = Path.Combine(args.WorkPath,
execution.TrackingNumber.ToString());
await _httpClient.GetAndSave(args.ImageUri, imageSavePath);
return execution.ReviseItinerary(builder =>
{
// add activity at the beginning of the current itinerary
builder.AddActivity("Deviation", new Uri($"exchange:{optionalAddress}"));
// maintain the existing activities
builder.AddActivitiesFromSourceItinerary();
// add activity at the end of the current itinerary
builder.AddActivity("Deviation", new Uri($"exchange:{optionalAddress}"));
});
}

By default, if an activity throws an exception, it will be faulted and a RoutingSlipFaulted event will be published (unless a subscription changes the rules). An activity can also return Faulted rather than throwing an exception.

async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
DownloadImageArguments args = execution.Arguments;
string imageSavePath = Path.Combine(args.WorkPath,
execution.TrackingNumber.ToString());
await _httpClient.GetAndSave(args.ImageUri, imageSavePath);
// will throw an exception
var result = 100 / 0;
return execution.Completed();
}

In the example above, the activity will throw an exception which will result in a Fault which will include data about the exception.

async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
DownloadImageArguments args = execution.Arguments;
string imageSavePath = Path.Combine(args.WorkPath,
execution.TrackingNumber.ToString());
await _httpClient.GetAndSave(args.ImageUri, imageSavePath);
return execution.Faulted();
}

In the example above, the activity can look at the data and then explicitly return a Faulted result.

In some situations, it may make sense to terminate the routing slip without executing any of the subsequent activities in the itinerary. This might be due to a business rule, in which the routing slip shouldn’t be faulted, but needs to end immediately.

To terminate a routing slip, call Terminate as shown.

async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
// regular termination
return execution.Terminate();
}

An optional reason can also be specified.

async Task<ExecutionResult> Execute(ExecuteContext<DownloadImageArguments> execution)
{
// terminate and include additional variables in the event
return execution.Terminate(new { Reason = "Not a good time, dude."});
}

Only IActivity requires you to implement the Compensate method. Compensate is called when a subsequent activity has faulted so that the activity can undo or reverse its effects.

When an activity fails, the Compensate method is called for previously executed activities in the routing slip that stored compensation data. If an activity does not store any compensation data, the Compensate method is never called.

return context.Completed(new LogModel {
SomeData = "abc"
})

The compensation method for the example above is shown below.

Task<CompensationResult> Compensate(CompensateContext<DownloadImageLog> compensation)
{
DownloadImageLog log = compensation.Log;
File.Delete(log.ImageSavePath);
return compensation.Compensated();
}

Using the activity log data, the activity compensates by removing the downloaded image from the work directory. Once the activity has successfully compensated for the previous execution, it returns a CompensationResult by calling the Compensated method. If the compensation cannot be completed (due to logic issues or exceptions) and this results in a failure, the Failed method should be used, optionally providing an Exception.