A POC to show NServiceBus Elsa activities
My goal was to convert the Saga tutorial in our docs to use dynamic Elsa workflows. The demo sends and receives messages and publishes events through Elsa activities. The workflows can be built by the designer at runtime. To run the POC start the ClientUI, Sales, and Billing projects and navigate to the https://localhost:5001/place-order URL (ClientUI project) to send the initial PlaceOrder
message. The Sales project should handle the message and publish an OrderPlaced
event which will be handled by the Billing project. I removed the Shipping project as I ran out of time before I could look at Sagas.
The code for Send a message
and Publish an event
activities can be found here and here. They both use the IMessageSession
from NServiceBus to send whatever is passed into the activity as input from the workflow context (see the Elsa documentation on workflow variables and workflow context).
Both the ClientUI and Sales projects implemented their own Elsa activity for instantiating message and event instances. ClientUI implemented the CreatePlaceOrderMessage activity, and Sales implemented the CreateOrderPlacedEvent activity. I am avoiding creating instances of arbitrary types at runtime using a JSON blob and a type string for the sake of time.
The code for the activity can be found here. This activity is a trigger type that signals Elsa to create a new workflow when an event occurs. When a message is received through NServiceBus the workflow is signaled from a pipeline behavior. Elsa uses a concept called Bookmarks to differentiate between multiple running workflows of the same type. In this case, a bookmark for the specific message type is created when the workflow is initialized.
The ClientUI and Sales workflows were created using the designer. There isn't any code in the projects that send or receive the PlaceOrder
/OrderPlaced
messages, it is all done 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. This means that the designer is able to publish workflows at runtime without the need to recycle the endpoint.
One downside to the default designer is that it can only point to one of the endpoints at a time. The endpoint to which the designer is pointed is controlled in the _Host.cshtml
file located here.
(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.
While I was successful in proving that events and messages can be sent and received using Elsa activities, there are several things that I was not able to test. Here is a list of things to consider before any of this could be "production-ready".
While this may not be a problem, pipeline behavior execution order is considered non-deterministic within the same stage, so it is conceivable that important downstream behaviors will be skipped when an Elsa workflow is found. In this implementation, the pipeline had to be short-circuited to avoid NServiceBus throwing an exception due to missing handler classes.
I did not have time to test how Elsa workflows fail or bubble out exceptions. It is possible a workflow fails silently and causes NServiceBus to ACK a message that should be retried or failed. If Elsa integration is something to continue pursuing, a concerted effort toward failure and fault tolerance will be an important step.
In this implementation, each endpoint is its own workflow server which was awkward when using the designer. To work well with multiple endpoints, it is advisable to build a solution to support viewing multiple workflow servers (if there isn't one already) or figure out how the endpoints can collaborate with a single workflow server.
In this implementation, I opted to make activities for each message type that instantiates them at runtime. This was very straightforward and simple given the extremely small number of message types (2 in this case). In a larger-scale implementation, something a bit more dynamic could make things less tedious, though likely more complex to get things right.