Integration with Elsa

This sample demonstrates how to use NServiceBus message handlers in Elsa workflows.

Overview

The demo is similar to the saga tutorial using dynamic Elsa workflows instead of a saga. Elsa activities are used to send and receive messages, and to publish events. The workflows can be built by the designer at runtime.

Running the project

To run the sample, start the ClientUI, Sales, and Billing projects and navigate to https://localhost:5001/place-order to send an initial PlaceOrder message. The Sales endpoint handles the message and publishes an OrderPlaced event which will be handled by the Billing endpoint.

Send a message and Publish an event activities

The code for the Send a message activity is as follows:

[Activity(
    Category = "NServiceBus",
    DisplayName = "Send a message",
    Description = "Sends a message over the bus",
    Outcomes = new[] { OutcomeNames.Done })]
public class SendNServiceBusMessage : Activity
{
    private readonly IMessageSession _messageSession;

    public SendNServiceBusMessage(IMessageSession messageSession)
    {
        _messageSession = messageSession;
    }

    [ActivityInput(Hint = "The name of the endpoint to which this message should be sent")]
    public string EndpointAddress { get; set; }

    protected override bool OnCanExecute(ActivityExecutionContext context)
    {
        return context.Input != null && EndpointAddress != null;
    }

    protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
    {
        if (context.Input != null)
        {
            await _messageSession.Send(EndpointAddress, context.Input);
        }

        return Done(context.Input);
    }
}

The activity uses the IMessageSession from NServiceBus to send the data that is passed into the activity as input from the workflow context (see the Elsa documentation on workflow variables and workflow context).

The code for the Publish an event activity follows a similar pattern:

[Activity(
    Category = "NServiceBus",
    DisplayName = "Publish an event",
    Description = "Publishes an event over the bus",
    Outcomes = new[] { OutcomeNames.Done })]
public class PublishNServiceBusEvent : Activity
{
    private readonly IMessageSession _messageSession;

    public PublishNServiceBusEvent(IMessageSession messageSession)
    {
        _messageSession = messageSession;
    }

    protected override bool OnCanExecute(ActivityExecutionContext context)
    {
        return context.Input != null;
    }

    protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
    {
        if (context.Input != null)
        {
            await _messageSession.Publish(context.Input);
        }

        return Done(context.Input);
    }
}

Both the ClientUI and Sales projects implement their own Elsa activities for instantiating message and event instances: CreatePlaceOrderMessage for the ClientUI, and CreateOrderPlacedEvent for the Sales endpoint.

Receiving a message in an activity

When a message is received through NServiceBus, the Elsa workflow is triggered from a pipeline behavior in CustomElsaHandlerTrigger.cs:

if (workflowRunner != null)
{
    // This bookmark distinguishes between different message types
    // for the same Activity
    var bookmark = new MessageReceivedBookmark(
        messageType.AssemblyQualifiedName);
    var query = new WorkflowsQuery(
        nameof(NServiceBusMessageReceived), bookmark);

    // Find any workflows that are suspended or triggered by this
    // event type and execute them in-process
    // The message instance is passed into the workflow as "Input"
    var workflowsFound = await workflowRunner
        .CollectAndExecuteWorkflowsAsync(query,
            new WorkflowInput( Input: context.Message.Instance));

    // If workflows were found, do not continue the pipeline.
    // In this case, there aren't any IHandleMessages implementations
    // defined and NServiceBus will throw an exception if the
    // pipeline continues.
    if (workflowsFound.Any())
    {
        return;
    }

    // If no workflows are found, continue the pipeline as normal.
    // This allows normal message processing if the handlers aren't
    // defined at runtime by Elsa.
    await next();
}

An Elsa bookmark is created when the workflow is initialized. The bookmark allows Elsa to distinguish between received messages of different types. After the bookmark is created, the behavior searches for workflows defined for a specific message type. If a workflow is found, it is executed immediately. Otherwise, the NServiceBus pipeline continues as normal.

Using the designer

The ClientUI and Sales workflows were created using the designer; all NServiceBus messages are sent or received through Elsa. The workflows can be inspected visually by starting the ElsaDesigner project and navigating to the "Workflow Definitions" page. The code in this project is pulled directly from the Elsa designer tutorial.

The ClientUI and the Sales endpoints are both configured as Elsa API endpoints. The designer is able to publish workflows at runtime without the need to recycle the endpoint.

Note that the default designer can point to only one endpoint at a time. The _Host.cshtml file controls which endpoint the designer is pointing at. In the sample, the ClientUI project runs on port 5001, and the Sales project runs on port 6001.

The workflows are saved in a Sqlite database (elsa.sqlite.db) in each of the Client and Sales project directories.


Last modified