Skip to main content
Version: 0.25

Setting up your first Development Project

Approximate time to complete: 1 hour

The Zeebe C# Client is available for .NET Zeebe applications.

Watch a video tutorial on YouTube walking through this Getting Started Guide.

Prerequisites#

Scaffolding the project#

Video link

  • Create a new .NET Core Web API application:
dotnet new webapi -o Cloudstartercd Cloudstarter
dotnet add package zb-client --version 0.16.1

Configure NLog for logging

Video link

  • Install NLog packages (we'll use NLog):
dotnet add package NLogdotnet add package NLog.Schemadotnet 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();}

Create Camunda Cloud cluster#

  • Log in to https://camunda.io.
  • Create a new Zeebe Cluster.
  • When the new cluster appears in the console, create a new set of client credentials.
  • Copy the client Connection Info environment variables block.

Configure connection#

Video link

  • Add the dotenv.net package to the project:
dotnet add package dotenv.net.DependencyInjection.Microsoft
  • Edit Startup.cs and add the service in the ConfigureServices 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 the export from each line:
ZEEBE_ADDRESS=656a9fc4-c874-49a3-b67b-20c31ae12fa0.zeebe.camunda.io:443ZEEBE_CLIENT_ID=~2WQlDeV1yFdtePBRQgsrNXaKMs4IwAwZEEBE_CLIENT_SECRET=3wFRuCJb4YPcKL4W9Fn7kXlsepSNNJI5h7Mlkqxk2E.coMEtYdA5E58lnkCmoN_0ZEEBE_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 in CloudStarter.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.

Test Connection with Camunda Cloud#

Video link

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 the ZeebeService class into the service container in the ConfigureServices 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.

You will see the topology response from the cluster.

Create a BPMN model#

  • 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 to get-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.

Deploy the BPMN model to Camunda Cloud#

Video Link

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 the ItemGroup:
<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 a Deploy 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 the Configure method:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){    var zeebeService = app.ApplicationServices.GetService<IZeebeService>();    zeebeService.Deploy("test-process.bpmn");    // ...}

Start a Workflow Instance#

Video Link

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 a StartWorkflowInstance 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;    }}

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.

View a Workflow Instance 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.

Create a Job Worker#

Video Link

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 the ZeebeService 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 in Startup.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.

Create and Await the Outcome of a Workflow Instance#

Video link

We will now create the workflow instance, and get the final outcome in the calling code.

  • Edit the ZeebeService.cs file, and edit the StartWorkflowInstance 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);}

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":"{}"}

Call a REST Service from the Worker#

Video link

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 the CreateGetTimeWorker 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();                }            }    });}// ...

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}}"}

Make a Decision#

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 to make-greeting.
  • Switch to the Headers tab on that Task, and create a new Key greeting with the Value Good morning.
  • In the second, set the Name to After noon and the Type to make-greeting.
  • Switch to the Headers tab on that Task, and create a new Key greeting with the Value Good 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:

Create a Worker that acts based on Custom Headers#

Video link

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 the ZeebeService:
public void StartWorkers(){    CreateGetTimeWorker();    CreateMakeGreetingWorker();}
  • Edit the startWorkflowInstance method, and pass in a variable name 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).

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}}"}

Profit!#

Congratulations. You've completed the Getting Started Guide for Camunda Cloud.