The Zeebe C# Client is available for .NET Zeebe applications.
Watch a video tutorial on YouTube walking through this Getting Started Guide.
- Create a new .NET Core Web API application:
dotnet new webapi -o Cloudstarter
cd Cloudstarter
- Add the Zeebe C# Client from Nuget:
dotnet add package zb-client --version 0.16.1
- Install NLog packages (we'll use NLog):
dotnet add package NLog
dotnet add package NLog.Schema
dotnet add package NLog.Web.AspNetCore
- Create a file
NLog.config
, with the following content:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
>
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<targets>
<target name="logconsole" xsi:type="Console"
layout="${longdate} | ${level:uppercase=true} | ${logger} | ${message} ${exception:format=tostring}"/>
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="logconsole" />
</rules>
</nlog>
- Edit the file
Program.cs
to configure NLog:
public class Program
{
public static async Task Main(string[] args)
{
var logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger();
try
{
logger.Debug("init main");
await CreateHostBuilder(args).Build().RunAsync();
}
catch (Exception exception)
{
logger.Error(exception, "Stopped program because of exception");
throw;
}
finally
{
NLog.LogManager.Shutdown();
}
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.Trace);
})
.UseNLog();
}
- Log in to https://camunda.io.
- Create a new Zeebe 0.23.3 cluster.
- When the new cluster appears in the console, create a new set of client credentials.
- Copy the client Connection Info environment variables block.
- Add the
dotenv.net
package to the project:
dotnet add package dotenv.net.DependencyInjection.Microsoft
- Edit
Startup.cs
and add the service in theConfigureServices
method:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddEnv(builder => {
builder
.AddEnvFile("CamundaCloud.env")
.AddThrowOnError(false)
.AddEncoding(Encoding.ASCII);
});
services.AddEnvReader();
}
- Create a file in the root of the project
CamundaCloud.env
, and paste the client connection details into it, removing theexport
from each line:
ZEEBE_ADDRESS=656a9fc4-c874-49a3-b67b-20c31ae12fa0.zeebe.camunda.io:443
ZEEBE_CLIENT_ID=~2WQlDeV1yFdtePBRQgsrNXaKMs4IwAw
ZEEBE_CLIENT_SECRET=3wFRuCJb4YPcKL4W9Fn7kXlsepSNNJI5h7Mlkqxk2E.coMEtYdA5E58lnkCmoN_0
ZEEBE_AUTHORIZATION_SERVER_URL=https://login.cloud.camunda.io/oauth/token
Note: if you change cluster configuration at a later date, you may need to delete the file ~/zeebe/cloud.token
. See this bug report.
- Add an
ItemGroup
inCloudStarter.csproj
to copy the.env
file into the build:
<ItemGroup>
<None Update="CamundaCloud.env" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
- Create a file in
Services/ZeebeService.cs
, with the following content:
namespace Cloudstarter.Services
{
public interface IZeebeService
{
public Task<ITopology> Status();
}
public class ZeebeService: IZeebeService
{
private readonly IZeebeClient _client;
private readonly ILogger<ZeebeService> _logger;
public ZeebeService(IEnvReader envReader, ILogger<ZeebeService> logger)
{
_logger = logger;
var authServer = envReader.GetStringValue("ZEEBE_AUTHORIZATION_SERVER_URL");
var clientId = envReader.GetStringValue("ZEEBE_CLIENT_ID");
var clientSecret = envReader.GetStringValue("ZEEBE_CLIENT_SECRET");
var zeebeUrl = envReader.GetStringValue("ZEEBE_ADDRESS");
char[] port =
{
'4', '3', ':'
};
var audience = zeebeUrl?.TrimEnd(port);
_client =
ZeebeClient.Builder()
.UseGatewayAddress(zeebeUrl)
.UseTransportEncryption()
.UseAccessTokenSupplier(
CamundaCloudTokenProvider.Builder()
.UseAuthServer(authServer)
.UseClientId(clientId)
.UseClientSecret(clientSecret)
.UseAudience(audience)
.Build())
.Build();
}
public Task<ITopology> Status()
{
return _client.TopologyRequest().Send();
}
}
}
- Save the file.
We will create a controller route at /status
that retrieves the status and topology of the cluster.
- Create a file
Controllers/ZeebeController.cs
, with the following content:
namespace Cloudstarter.Controllers
{
public class ZeebeController : Controller
{
private readonly IZeebeService _zeebeService;
public ZeebeController(IZeebeService zeebeService)
{
_zeebeService = zeebeService;
}
[Route("/status")]
[HttpGet]
public async Task<string> Get()
{
return (await _zeebeService.Status()).ToString();
}
}
}
- Edit the file
Startup.cs
, and inject theZeebeService
class into the service container in theConfigureServices
method, like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IZeebeService, ZeebeService>();
services.AddControllers();
}
- Run the application with the command
dotnet run
(remember to set the client connection variables in the environment first).
Note: you can use dotnet watch run
to automatically restart your application when you change your code.
- Open http://localhost:5000/status in your web browser.
You will see the topology response from the cluster.
- Download and install the Zeebe Modeler.
- Open Zeebe Modeler and create a new BPMN Diagram.
- Create a new BPMN diagram.
- Add a StartEvent, an EndEvent, and a Task.
- Click on the Task, click on the little spanner/wrench icon, and select "Service Task".
- Set the Name of the Service Task to
Get Time
, and the Type toget-time
.
It should look like this:
- Click on the blank canvas of the diagram, and set the Id to
test-process
, and the Name to "Test Process". - Save the diagram to
Resources/test-process.bpmn
in your project.
We need to copy the bpmn file into the build, so that it is available to our program at runtime.
- Edit the
Cloudstarter.csproj
file, and add the following to theItemGroup
:
<ItemGroup>
<None Update="Resources\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Now we create a method in our service to deploy a bpmn model to the cluster.
- Edit
ZeebeService.cs
, and add aDeploy
method:
public async Task<IDeployResponse> Deploy(string modelFilename)
{
var filename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, "Resources", modelFilename);
var deployment = await _client.NewDeployCommand().AddResourceFile(filename).Send();
var res = deployment.Workflows[0];
_logger.LogInformation("Deployed BPMN Model: " + res?.BpmnProcessId +
" v." + res?.Version);
return deployment;
}
- In the
ZeebeService.cs
file, update the interface definition:
public interface IZeebeService
{
public Task<IDeployResponse> Deploy(string modelFilename);
public Task<ITopology> Status();
}
Now, we call the Deploy
method during the initialization of the service at startup. We need to do it here, because the service is not instantiated
- Edit
Startup.cs
, and add the following lines to theConfigure
method:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var zeebeService = app.ApplicationServices.GetService<IZeebeService>();
zeebeService.Deploy("test-process.bpmn");
// ...
}
We will create a controller route at /start
that will start a new instance of the workflow.
- Add fastJSON to the project:
dotnet add package fastJSON
- Edit
Services/ZeebeService.cs
and add aStartWorkflowInstance
method:
public async Task<String> StartWorkflowInstance(string bpmProcessId)
{
var instance = await _client.NewCreateWorkflowInstanceCommand()
.BpmnProcessId(bpmProcessId)
.LatestVersion()
.Send();
var jsonParams = new JSONParameters {ShowReadOnlyProperties = true};
return JSON.ToJSON(instance, jsonParams);
}
- Update the service interface definition:
public interface IZeebeService
{
public Task<IDeployResponse> Deploy(string modelFile);
public Task<ITopology> Status();
public Task<String> StartWorkflowInstance(string bpmProcessId);
}
- Edit
Controllers/ZeebeController.cs
, and add a REST method to start an instance of the workflow:
// ...
public class ZeebeController : Controller
// ...
[Route("/start")]
[HttpGet]
public async Task<string> StartWorkflowInstance()
{
var instance = await _zeebeService.StartWorkflowInstance("test-process");
return instance;
}
}
-
Run the program with the command:
dotnet run
. -
Visit http://localhost:5000/start in your browser.
You will see output similar to the following:
{"$types":{"Zeebe.Client.Impl.Responses.WorkflowInstanceResponse, Client, Version=0.16.1.0, Culture=neutral, PublicKeyToken=null":"1"},"$type":"1","WorkflowKey":2251799813685454,"BpmnProcessId":"test-process","Version":3,"WorkflowInstanceKey":2251799813686273}
A workflow instance has been started. Let's view it in Operate.
- Go to your cluster in the Camunda Cloud Console.
- In the cluster detail view, click on "View Workflow Instances in Camunda Operate".
- In the "Instances by Workflow" column, click on "Test Process - 1 Instance in 1 Version".
- Click the Instance Id to open the instance.
- You will see the token is stopped at the "Get Time" task.
Let's create a task worker to serve the job represented by this task.
We will create a worker program that logs out the job metadata, and completes the job with success.
- Edit the
Services/ZeebeService.cs
file, and add a_createWorker
method to theZeebeService
class:
// ...
private void _createWorker(String jobType, JobHandler handleJob)
{
_client.NewWorker()
.JobType(jobType)
.Handler(handleJob)
.MaxJobsActive(5)
.Name(jobType)
.PollInterval(TimeSpan.FromSeconds(50))
.PollingTimeout(TimeSpan.FromSeconds(50))
.Timeout(TimeSpan.FromSeconds(10))
.Open();
}
- Now add a
CreateGetTimeWorker
method, where we supply the task-type for the worker, and a job handler function:
public void CreateGetTimeWorker()
{
_createWorker("get-time", async (client, job) =>
{
_logger.LogInformation("Received job: " + job);
await client.NewCompleteJobCommand(job.Key).Send();
});
}
The worker handler function is async
so that it runs on its own thread.
- Now create a method
StartWorkers
:
public void StartWorkers()
{
CreateGetTimeWorker();
}
- And add it to the
IZeebeService
interface:
public interface IZeebeService
{
public Task<IDeployResponse> Deploy(string modelFile);
public Task<ITopology> Status();
public Task<string> StartWorkflowInstance(string bpmProcessId);
public void StartWorkers();
}
- Now call this method in the
Configure
method inStartup.cs
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var zeebeService = app.ApplicationServices.GetService<IZeebeService>();
zeebeService.Deploy("test-process.bpmn");
zeebeService.StartWorkers();
// ...
}
- Run the program with the command:
dotnet run
.
You will see output similar to:
2020-07-16 20:34:25.4971 | DEBUG | Zeebe.Client.Impl.Worker.JobWorker | Job worker (get-time) activated 1 of 5 successfully.
2020-07-16 20:34:25.4971 | INFO | Cloudstarter.Services.ZeebeService | Received job: Key: 2251799813686173, Type: get-time, WorkflowInstanceKey: 2251799813686168, BpmnProcessId: test-process, WorkflowDefinitionVersion: 3, WorkflowKey: 2251799813685454, ElementId: Activity_1ucrvca, ElementInstanceKey: 2251799813686172, Worker: get-time, Retries: 3, Deadline: 07/16/2020 20:34:35, Variables: {}, CustomHeaders: {}
- Go back to Operate. You will see that the workflow instance is gone.
- Click on "Running Instances".
- In the filter on the left, select "Finished Instances".
You will see the completed workflow instance.
We will now create the workflow instance, and get the final outcome in the calling code.
- Edit the
ZeebeService.cs
file, and edit theStartWorkflowInstance
method, to make it look like this:
// ...
public async Task<String> StartWorkflowInstance(string bpmProcessId)
{
var instance = await _client.NewCreateWorkflowInstanceCommand()
.BpmnProcessId(bpmProcessId)
.LatestVersion()
.WithResult()
.Send();
var jsonParams = new JSONParameters {ShowReadOnlyProperties = true};
return JSON.ToJSON(instance, jsonParams);
}
-
Run the program with the command:
dotnet run
. -
Visit http://localhost:5000/start in your browser.
You will see output similar to the following:
{"$types":{"Zeebe.Client.Impl.Responses.WorkflowInstanceResultResponse, Client, Version=0.16.1.0, Culture=neutral, PublicKeyToken=null":"1"},"$type":"1","WorkflowKey":2251799813686366,"BpmnProcessId":"test-process","Version":4,"WorkflowInstanceKey":2251799813686409,"Variables":"{}"}
We are going to make a REST call in the worker handler, to query a remote API for the current GMT time.
- Edit the
ZeebeService.cs
file, and edit theCreateGetTimeWorker
method, to make it look like this:
// ...
public void CreateGetTimeWorker()
{
_createWorker("get-time", async (client, job) =>
{
_logger.LogInformation("Received job: " + job);
using (var httpClient = new HttpClient())
{
using (var response = await httpClient.GetAsync("https://json-api.joshwulf.com/time"))
{
string apiResponse = await response.Content.ReadAsStringAsync();
await client.NewCompleteJobCommand(job.Key)
.Variables("{\"time\":" + apiResponse + "}")
.Send();
}
}
});
}
// ...
- Run the program with the command:
dotnet run
. - Visit http://localhost:5000/start in your browser.
You will see output similar to the following:
{"$types":{"Zeebe.Client.Impl.Responses.WorkflowInstanceResultResponse, Client, Version=0.16.1.0, Culture=neutral, PublicKeyToken=null":"1"},"$type":"1","WorkflowKey":2251799813686366,"BpmnProcessId":"test-process","Version":4,"WorkflowInstanceKey":2251799813686463,"Variables":"{\"time\":{\"time\":\"Thu, 16 Jul 2020 10:26:13 GMT\",\"hour\":10,\"minute\":26,\"second\":13,\"day\":4,\"month\":6,\"year\":2020}}"}
We will edit the model to add a Conditional Gateway.
- Open the BPMN model file
bpmn/test-process.bpmn
in the Zeebe Modeler. - Drop a Gateway between the Service Task and the End event.
- Add two Service Tasks after the Gateway.
- In one, set the Name to
Before noon
and the Type tomake-greeting
. - Switch to the Headers tab on that Task, and create a new Key
greeting
with the ValueGood morning
. - In the second, set the Name to
After noon
and the Type tomake-greeting
. - Switch to the Headers tab on that Task, and create a new Key
greeting
with the ValueGood afternoon
. - Click on the arrow connecting the Gateway to the Before noon task.
- Under Details enter the following in Condition expression:
=time.hour >=0 and time.hour <=11
- Click on the arrow connecting the Gateway to the After noon task.
- Click the spanner/wrench icon and select "Default Flow".
- Connect both Service Tasks to the End Event.
It should look like this:
We will create a second worker that combines the value of a custom header with the value of a variable in the workflow.
- Edit the
ZeebeService.cs
file and create a couple of DTO classes to aid with deserialization of the job:
public class MakeGreetingCustomHeadersDTO
{
public string greeting { get; set; }
}
public class MakeGreetingVariablesDTO
{
public string name { get; set; }
}
- In the same file, create a
CreateMakeGreetingWorker
method:
public void CreateMakeGreetingWorker()
{
_createWorker("make-greeting", async (client, job) =>
{
_logger.LogInformation("Make Greeting Received job: " + job);
var headers = JSON.ToObject<MakeGreetingCustomHeadersDTO>(job.CustomHeaders);
var variables = JSON.ToObject<MakeGreetingVariablesDTO>(job.Variables);
string greeting = headers.greeting;
string name = variables.name;
await client.NewCompleteJobCommand(job.Key)
.Variables("{\"say\": \"" + greeting + " " + name + "\"}")
.Send();
_logger.LogInformation("Make Greeting Worker completed job");
});
}
- Now call this method in the
StartWorkers
method of theZeebeService
:
public void StartWorkers()
{
CreateGetTimeWorker();
CreateMakeGreetingWorker();
}
- Edit the
startWorkflowInstance
method, and pass in a variablename
when you create the workflow:
// ...
public async Task<String> StartWorkflowInstance(string bpmProcessId)
{
var instance = await _client.NewCreateWorkflowInstanceCommand()
.BpmnProcessId(bpmProcessId)
.LatestVersion()
.Variables("{\"name\": \"Josh Wulf\"}")
.WithResult()
.Send();
var jsonParams = new JSONParameters {ShowReadOnlyProperties = true};
return JSON.ToJSON(instance, jsonParams);
}
You can change the variable name
value to your own name (or derive it from the url path or a parameter).
- Run the program with the command:
dotnet run
. - Visit http://localhost:5000/start in your browser.
You will see output similar to the following:
{"$types":{"Zeebe.Client.Impl.Responses.WorkflowInstanceResultResponse, Client, Version=0.16.1.0, Culture=neutral, PublicKeyToken=null":"1"},"$type":"1","WorkflowKey":2251799813686683,"BpmnProcessId":"test-process","Version":5,"WorkflowInstanceKey":2251799813687157,"Variables":"{\"say\":\"Good Afternoon Josh Wulf\",\"name\":\"Josh Wulf\",\"time\":{\"time\":\"Thu, 16 Jul 2020 12:45:33 GMT\",\"hour\":12,\"minute\":45,\"second\":33,\"day\":4,\"month\":6,\"year\":2020}}"}
Congratulations. You've completed the Getting Started Guide for Camunda Cloud using C# and ASP .NET Core.