Quickstart

Elsa Server

In this quickstart, we will take a look at a minimum ASP.NET Core application that sets up an Elsa Server. We will also install some more commonly used activities such as Timer, Cron and SendEmail to implement simple recurring workflows.

Server Only

The purpose of this application is to be a workflow server. This means that it will host and execute workflows, but it will not host the dashboard UI. Setting up the dashboard UI is explained here. It's of course also possible to combine the workflow host with a web app that hosts the dashboard UI, which is explained here.

We will:

  • Create an ASP.NET Core application.
  • Configure a persistence provider with EF Core and the SQLite provider.
  • Register various activities for use in workflows.
  • Create a simple workflow using the Workflow Builder API.
  • Expose the Elsa API Endpoints for consumption by external applications (including the Elsa Dashboard).
  • Try out the Elsa API Endpoints using Postman.

The Project

Create a new, empty ASP.NET Core project called ElsaQuickstarts.Server.ApiEndpoints:

dotnet new web -n "ElsaQuickstarts.Server.ApiEndpoints"

CD into the created project folder:

cd ElsaQuickstarts.Server.ApiEndpoints

Add the following packages:

dotnet add package Elsa
dotnet add package Elsa.Activities.Http
dotnet add package Elsa.Activities.Temporal.Quartz
dotnet add package Elsa.Persistence.EntityFramework.Sqlite
dotnet add package Elsa.Server.Api

Heartbeat Workflow

Just for demo purposes, we will create a simple "heartbeat" workflow that will write the current time to standard at a regular interval. This will show us a number of things:

  1. Elsa supports both programmatic as well as dynamic workflows (e.g. created visually using the Elsa Dashboard).
  2. If we have the Elsa Dashboard setup, we can view this Heartbeat workflow visually, even though we created it here programmatically.
  3. We can interact with the Elsa API endpoints using Postman and query the workflow instances generated by the Heartbeat workflow.

Go ahead and create a new file called HeartbeatWorkflow.cs and add the following code:

using Elsa.Activities.Console;
using Elsa.Activities.Temporal;
using Elsa.Builders;
using NodaTime;

namespace ElsaQuickstarts.Server.ApiEndpoints
{
    public class HeartbeatWorkflow : IWorkflow
    {
        private readonly IClock _clock;
        public HeartbeatWorkflow(IClock clock) => _clock = clock;

        public void Build(IWorkflowBuilder builder) =>
            builder
                .Timer(Duration.FromSeconds(10))
                .WriteLine(context => $"Heartbeat at {_clock.GetCurrentInstant()}");
    }
}

The above workflow has two activities. The first activity Timer will cause this workflow to execute every 10 seconds. The second activity WriteLine writes the current time to standard out. Notice that it uses an overload that takes a delegate instead of a string literal. This allows you to provide property values at runtime that are dynamic. This is similar to providing workflow expressions using JavaScript and Liquid expressions in Elsa 1. The difference of course being that you can now use plain old C# syntax when writing workflows using the Workflow Builder API as is shown here.

Notice also that the HeartbeatWorkflow class can accept constructor-injected services, just like any other type you would register with the DI system.

In case you are wondering about IClock - it's an abstraction provided by NodaTime, an alternative date and time API for .NET.

Startup

Next, open Startup.cs and replace its contents with the following:

using Elsa;
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Elsa.Persistence.EntityFramework.Sqlite;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ElsaQuickstarts.Server.ApiEndpoints
{
    public class Startup
    {
        public Startup(IWebHostEnvironment environment, IConfiguration configuration)
        {
            Environment = environment;
            Configuration = configuration;
        }

        private IWebHostEnvironment Environment { get; }
        private IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            var elsaSection = Configuration.GetSection("Elsa");

            // Elsa services.
            services
                .AddElsa(elsa => elsa
                    .UseEntityFrameworkPersistence(ef => ef.UseSqlite())
                    .AddConsoleActivities()
                    .AddHttpActivities(elsaSection.GetSection("Server").Bind)
                    .AddQuartzTemporalActivities()
                    .AddJavaScriptActivities()
                    .AddWorkflowsFrom<Startup>()
                );

            // Elsa API endpoints.
            services.AddElsaApiEndpoints();

            // Allow arbitrary client browser apps to access the API.
            // In a production environment, make sure to allow only origins you trust.
            services.AddCors(cors => cors.AddDefaultPolicy(policy => policy
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowAnyOrigin()
                .WithExposedHeaders("Content-Disposition"))
            );
        }

        public void Configure(IApplicationBuilder app)
        {
            if (Environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app
                .UseCors()
                .UseHttpActivities()
                .UseRouting()
                .UseEndpoints(endpoints =>
                {
                    // Elsa API Endpoints are implemented as regular ASP.NET Core API controllers.
                    endpoints.MapControllers();
                });
        }
    }
}

When using the Entity Framework Core provider, Elsa will use pooled DB contexts by default and will automatically run migrations for you. If you do not wish to use pooled DB contexts, use the UseNonPooledEntityFrameworkPersistence method instead. If you prefer to run the migrations yourself, make sure to pass autoRunMigrations: false when using the UseEntityFrameworkPersistence method (it is an optional parameter set to true by default).

Notice that we're accessing a configuration section called "Elsa". We then use this section to retrieve sub-sections called "Http" and "Smtp"". Let's update appsettings.json with these sections next:

Appsettings.json

Open appsettings.json and add the following section:

{
  "Elsa": {
    "Http": {
      "BaseUrl": "https://localhost:5001"
    }
  }
}

The reason we are setting a "base URL" is because the HTTP activities library provides an absolute URL provider that can be used by activities and workflow expressions. Since this absolute URL provider can be used outside the context of an actual HTTP request (for instance, when a timer event occurs), we cannot rely on e.g. IHttpContextAccessor, since there won't be any HTTP context.

Run

Run the program and wait until you see the following output:

Now listening on: http://localhost:5000
Now listening on: https://localhost:5001
Application started. Press Ctrl+C to shut down.

If you wait for about 10 seconds, you should start seeing the following output:

info: Elsa.Bookmarks.BookmarkIndexer[0]
      Indexed 0 bookmarks in 00:00:00.0077348
Heartbeat at 2021-05-07T19:43:47Z
info: Elsa.Bookmarks.BookmarkIndexer[0]
      Indexing bookmarks

Postman

Start Postman or any other tool that enables you to send HTTP requests. We'll now try out some of the exposed APIs.

List Workflow Blueprints

First, let's query the workflow registry:

GET /v1/workflow-registry
Host: localhost:5001

The JSON response will include a "summary" view of all workflows registered (currently just one):

{
    "items": [
        {
            "id": "HeartbeatWorkflow",
            "name": "HeartbeatWorkflow",
            "displayName": "HeartbeatWorkflow",
            "description": null,
            "version": 1,
            "tenantId": null,
            "isSingleton": false,
            "isEnabled": false,
            "isPublished": true,
            "isLatest": true
        }
    ],
    "page": null,
    "pageSize": null,
    "totalCount": 1
}

Get Single Workflow Blueprint

To get the full workflow blueprint definition, issue the following HTTP request:

GET /v1/workflow-registry/HeartbeatWorkflow
Host: localhost:5001

The response will include more detail, including activities and connections:

{
    "$id": "1",
    "version": 1,
    "isSingleton": false,
    "isEnabled": false,
    "isPublished": true,
    "isLatest": true,
    "variables": {
        "$id": "2",
        "data": {}
    },
    "persistenceBehavior": "WorkflowBurst",
    "deleteCompletedInstances": false,
    "customAttributes": {
        "$id": "3",
        "data": {}
    },
    "activities": [
        {
            "$id": "4",
            "id": "activity-1",
            "type": "Timer",
            "parentId": "HeartbeatWorkflow",
            "persistWorkflow": false,
            "loadWorkflowContext": false,
            "saveWorkflowContext": false,
            "properties": {
                "$id": "5",
                "data": {
                    "Timeout": "0:00:10"
                }
            }
        },
        {
            "$id": "6",
            "id": "activity-2",
            "type": "WriteLine",
            "parentId": "HeartbeatWorkflow",
            "persistWorkflow": false,
            "loadWorkflowContext": false,
            "saveWorkflowContext": false,
            "properties": {
                "$id": "7",
                "data": {
                    "Text": "Heartbeat at 2021-05-07T19:58:22Z"
                }
            }
        }
    ],
    "connections": [
        {
            "$id": "8",
            "sourceActivityId": "activity-1",
            "targetActivityId": "activity-2",
            "outcome": "Done"
        }
    ],
    "id": "HeartbeatWorkflow",
    "name": "HeartbeatWorkflow",
    "displayName": "HeartbeatWorkflow",
    "type": "HeartbeatWorkflow",
    "parentId": "HeartbeatWorkflow",
    "persistWorkflow": true,
    "loadWorkflowContext": false,
    "saveWorkflowContext": false,
    "properties": {
        "$id": "9",
        "data": {}
    }
}

List Workflow Instances

As the HeartbeatWorkflow executes, a new workflow instance will be created every 10 seconds. To get a list of workflow instances, issue the following request:

GET /v1/workflow-instances?workflow=HeartbeatWorkflow&page=0&pageSize=2
Host: localhost:5001

The response should look similar to this:

{
    "items": [
        {
            "id": "e380d0a7fd4a4b6ba236fbdc0adf0ddb",
            "definitionId": "HeartbeatWorkflow",
            "tenantId": null,
            "version": 1,
            "workflowStatus": "Finished",
            "correlationId": null,
            "contextType": null,
            "contextId": null,
            "name": null,
            "createdAt": "2021-05-07T19:43:46.4198083Z",
            "lastExecutedAt": "2021-05-07T19:43:47.4602325Z",
            "finishedAt": "2021-05-07T19:43:47.4626325Z",
            "cancelledAt": null,
            "faultedAt": null
        },
        {
            "id": "418d0b535a89413e9ca2014a3b476b93",
            "definitionId": "HeartbeatWorkflow",
            "tenantId": null,
            "version": 1,
            "workflowStatus": "Finished",
            "correlationId": null,
            "contextType": null,
            "contextId": null,
            "name": null,
            "createdAt": "2021-05-07T19:43:56.175008Z",
            "lastExecutedAt": "2021-05-07T19:43:56.3284055Z",
            "finishedAt": "2021-05-07T19:43:56.3285439Z",
            "cancelledAt": null,
            "faultedAt": null
        }
    ],
    "page": 0,
    "pageSize": 2,
    "totalCount": 110
}

List Activities

To get a full list of available activities, you can issue the following HTTP request:

GET /v1/activities
Host: localhost:5001

The following is a partial response to give you an idea of what it looks like:

[
    {
        "type": "ReadLine",
        "displayName": "Read Line",
        "description": "Read text from standard in.",
        "category": "Console",
        "traits": 1,
        "outcomes": [
            "Done"
        ],
        "properties": []
    },
    {
        "type": "WriteLine",
        "displayName": "Write Line",
        "description": "Write text to standard out.",
        "category": "Console",
        "traits": 1,
        "outcomes": [
            "Done"
        ],
        "properties": [
            {
                "name": "Text",
                "type": "System.String",
                "uiHint": "single-line",
                "label": "Text",
                "hint": "The text to write.",
                "supportedSyntaxes": [
                    "JavaScript",
                    "Liquid"
                ]
            }
        ]
    },
    {
        "type": "HttpEndpoint",
        "displayName": "HTTP Endpoint",
        "description": "Handle an incoming HTTP request.",
        "category": "HTTP",
        "traits": 2,
        "outcomes": [
            "Done"
        ],
        "properties": [
            {
                "name": "Path",
                "type": "Microsoft.AspNetCore.Http.PathString",
                "uiHint": "single-line",
                "label": "Path",
                "hint": "The relative path that triggers this activity.",
                "supportedSyntaxes": [
                    "JavaScript",
                    "Liquid"
                ]
            },
            {
                "name": "Methods",
                "type": "System.Collections.Generic.HashSet`1[System.String]",
                "uiHint": "check-list",
                "label": "Methods",
                "hint": "The HTTP methods that trigger this activity.",
                "options": [
                    "GET",
                    "POST",
                    "PUT",
                    "DELETE",
                    "PATCH",
                    "OPTIONS",
                    "HEAD"
                ],
                "defaultValue": [
                    "GET"
                ],
                "defaultSyntax": "Json",
                "supportedSyntaxes": [
                    "Json",
                    "JavaScript",
                    "Liquid"
                ]
            },
            {
                "name": "ReadContent",
                "type": "System.Boolean",
                "uiHint": "checkbox",
                "label": "Read Content",
                "hint": "A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. The stored format depends on the content-type header.",
                "supportedSyntaxes": [
                    "Literal",
                    "JavaScript",
                    "Liquid"
                ]
            },
            {
                "name": "TargetType",
                "type": "System.Type",
                "uiHint": "single-line",
                "label": "Target Type",
                "category": "Advanced",
                "supportedSyntaxes": []
            }
        ]
    }
]

Next Steps

In this guide, we've seen how to setup an Elsa Server Host that can host and execute workflows and exposes a set of API endpoints for use by external applications (such as the Elsa Dashboard, which we haven't seen here but is covered here).

Now that you've seen how to setup an ASP.NET Core server with Elsa workflows support, you might want to learn more about the following:

Previous
Hello World HTTP