Part 2 of Building Workflow Driven .NET Applications with Elsa 2
In the previous part, we introduced the sample solution we are building. In this part, we will scaffold parts of the solution up to the point where we have a basic Razor pages web app and an Elsa Server up and running that can execute workflows.
File -> New Project
Or, in other words:
dotnet new webapp -n DocumentManagement.Web -o DocumentManagement\src\DocumentManagement.Web -f net5.0
That will create a new ASP.NET Web Application with Razor Pages and Bootstrap 4. Although optional, I went ahead and updated to Bootstrap 5 for no other reason than that I simply could not help myself.
To upgrade to Bootstrap 5, simply download the distribution files and replace the js and css folders in \wwwroot\lib\bootstrap\dist.
In addition to creating the web project, we will also add three more class library projects:
- Core (for domain models and services)
- Persistence (for data access services)
- Workflows (for workflow-specifics such as hosting actual workflow JSON files and custom activities)
Execute the following commands to generate these projects:
dotnet new classlib -n DocumentManagement.Core -f netstandard2.1 --langVersion latest -o DocumentManagement\src\DocumentManagement.Coredotnet new classlib -n DocumentManagement.Persistence -f net5.0 --langVersion latest -o DocumentManagement\src\DocumentManagement.Persistencedotnet new classlib -n DocumentManagement.Workflows -f netstandard2.1 --langVersion latest -o DocumentManagement\src\DocumentManagement.Workflows
Create a solution file and add all four projects to it:
dotnet new sln -n DocumentManagement -o DocumentManagementdotnet sln DocumentManagement/DocumentManagement.sln add DocumentManagement/src/DocumentManagement.Web/DocumentManagement.Web.csproj DocumentManagement/src/DocumentManagement.Core/DocumentManagement.Core.csproj DocumentManagement/src/DocumentManagement.Persistence/DocumentManagement.Persistence.csproj DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj
Now that we have a solution structure, we will build up each project re-iteratively.
The web app will serve both the UI (using Razor Pages) and execute workflows using Elsa and Hangfire for background processing.
If you run the project now, you should see a default web app. Our goal now is to turn the project into an Elsa Server that can execute workflows and to which Elsa Studio can connect.
Elsa Studio is a set of client-side web components that provides a UI and a visual workflow designer to manage workflows.
Add the following project references and packages:
dotnet add DocumentManagement/src/DocumentManagement.Web/DocumentManagement.Web.csproj reference DocumentManagement/src/DocumentManagement.Core/DocumentManagement.Core.csproj DocumentManagement/src/DocumentManagement.Persistence/DocumentManagement.Persistence.csproj DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj
- Hangfire.AspNetCore (for hosting Hangfire workers in ASP.NET Core)
- Hangfire.SQLite (for persisting Hangfire jobs in the SQLite database)
- Storage.Net (for abstracting away file access to store uploaded files)
- Elsa.Persistence.EntityFramework.Sqlite (for persisting workflow instances in the SQLite database)
- Elsa.Server.Api (for exposing API Endpoints to Elsa Studio)
dotnet add DocumentManagement/src/DocumentManagement.Web/DocumentManagement.Web.csproj package Hangfire.AspNetCoredotnet add DocumentManagement/src/DocumentManagement.Web/DocumentManagement.Web.csproj package Hangfire.SQLitedotnet add DocumentManagement/src/DocumentManagement.Web/DocumentManagement.Web.csproj package Storage.Netdotnet add DocumentManagement/src/DocumentManagement.Web/DocumentManagement.Web.csproj package Elsa.Persistence.EntityFramework.Sqlitedotnet add DocumentManagement/src/DocumentManagement.Web/DocumentManagement.Web.csproj package Elsa.Server.Api
Update the Startup class by replacing its contents with the following code:
The code with comments should be mostly self-explanatory, but let’s go over some of the more interesting parts.
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("Content-Disposition")));
This registers CORS services, which is necessary only if we want to be able to connect a client applicationthat is hosted on a different origin than the one from which Elsa Server is hosted.
In our case, we want to be able to run Elsa Studio from a Docker container, which therefore will have a different origin.
This installs a middleware provided by the Elsa.Activities.Http package, and enables workflows to be exposed as API endpoints by using the HTTP Endpoint activity.
We haven’t made mention of this activity before, and it won’t actually be used in any of the three demo workflows. But we will use it to create a very simple “Hello World” workflow in this part.
Although our web application itself won’t expose any API endpoints, the Elsa.Server.Api package does provide a number of API controllers that are consumed by Elsa Studio.
With the upcoming .NET 6, the API endpoints are likely to be refactored to be using minimal APIs. This enables greater granularity when it comes to the host application controlling what API endpoints are exposed and what are not.
services.AddWorkflowServices(dbContext => dbContext.UseSqlite(dbConnectionString));
AddWorkflowServices extension method doesn’t currently exist, but we will implement this when we work on the
DocumentManagement.Workflows project. This method will be setting up Elsa services and activities.
The startup code references
Configuration in a number of places, so let’s update appsettings.json next as follows:
Notice that the
Elsa:Server:BaseUrl setting is configured with a specific URL — this URL must match the same URL with which your application is launched.
BaseUrlis used when Elsa needs to generate absolute URLs from background processes where there is no current HTTP context available to infer the base URL. Absolute URLs are used in scenarios such as sending emails containing links to trigger a signal (e.g. Approve & Reject links). If these emails are sent from a background task (for example, when triggered in response to a timer event), we don’t have an actual HTTP context. Having a base URL configured solves this problem nicely. It also helps when hosting your application on containers behind a reverse proxy for example.
Since we’ll be using the Send Email activity to send emails, we require a running SMTP server. We will host one using a Docker container using the following command:
docker run -p 3000:80 -p 2525:25 rnwood/smtp4dev:linux-amd64-3.1.0-ci0856
This will start Smtp4Dev, an awesome little SMTP server ideal for developing applications that need to send email messages which we want to intercept and inspect.
The SMTP host will listen for instructions at port 2525 and provides a web-based UI at port 3000. This UI will help us inspecting email messages without actually sending these messages to anyone.
Next, we will update the
This project will provide custom activities, workflows in the form of JSON files, handlers and utility functions for handling workflow-related business.
In this part, all we’ll be doing however is implementing an extension method to register Elsa with DI.
Add the following project & package references:
dotnet add DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj reference DocumentManagement/src/DocumentManagement.Core/DocumentManagement.Core.csproj
dotnet add DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj package Elsadotnet add DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj package Elsa.Activities.Emaildotnet add DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj package Elsa.Activities.Httpdotnet add DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj package Elsa.Persistence.EntityFramework.Coredotnet add DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj package Elsa.Server.Hangfiredotnet add DocumentManagement/src/DocumentManagement.Workflows/DocumentManagement.Workflows.csproj package Microsoft.EntityFrameworkCore
Create a new folder called
Extensions and add the following class:
At this point, the code should compile and we should be able to run the application using CTRL+F5:
More importantly, we should now be able to access the Elsa Server API. Try it out by issuing the following HTTP GET request (using e.g. Postman):
curl --location --request GET 'https://localhost:5001/v1/activities'
The response should be an array of activity descriptors. Example:
"displayName": "Read Line",
"description": "Read text from standard in.",
Before we can start designing workflows, we need to run Elsa Studio. The easiest way to do that is by running it using Docker.
Run the following command to start Elsa Studio on port 14000:
docker run -t -i -e ELSA__SERVER__BASEADDRESS=http://localhost:5000 -p 14000:80 elsaworkflows/elsa-dashboard:latest
http://localhost:14000 should result in the following page:
Let’s create a simple “Hello World” workflow to see that everything is working.
Click the Workflow Definitions menu item and then click the Create Workflow button at the top right.
On the designer, click the big green Start button to add the first activity, HTTP Endpoint, with the following configuration:
Connect a new activity of type HTTP Response and configure it as follows:
Before we publish the workflow, let’s first configure workflow definition settings (click the cog wheel at the far top right).
Provide the following configuration:
- Display Name:
Save the changes and then click the Publish button at the far bottom-right of the screen:
Executing the workflow
To try out the workflow, navigate to
http://localhost:5000/workflows/hello-world. You should see the following result:
We have just witnessed one of many cool features of Elsa: executing workflows can not only be done explicitly using various code-level services, but also implicitly through the use of workflow triggers such as HTTP Endpoint.
HTTP Endpoint effectively enables the implementation of API endpoints via workflows.
That’s it for this part. We setup a basic solution structure with an ASP.NET web application that serves UI and can execute workflows.
In the next part, we will see how we can save workflows as JSON files and execute them instead.