Temporalio 0.1.0-alpha2

This is a prerelease version of Temporalio.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Temporalio --version 0.1.0-alpha2
NuGet\Install-Package Temporalio -Version 0.1.0-alpha2
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Temporalio" Version="0.1.0-alpha2" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Temporalio --version 0.1.0-alpha2
#r "nuget: Temporalio, 0.1.0-alpha2"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Temporalio as a Cake Addin
#addin nuget:?package=Temporalio&version=0.1.0-alpha2&prerelease

// Install Temporalio as a Cake Tool
#tool nuget:?package=Temporalio&version=0.1.0-alpha2&prerelease

Temporal .NET SDK

NuGet MIT

Temporal is a distributed, scalable, durable, and highly available orchestration engine used to execute asynchronous, long-running business logic in a scalable and resilient way.

"Temporal .NET SDK" is the framework for authoring workflows and activities using .NET programming languages.

Also see:

⚠️ UNDER ACTIVE DEVELOPMENT

This SDK is under active development and has not released a stable version yet. APIs may change in incompatible ways until the SDK is marked stable.

Notably missing from this SDK:

  • Workflow workers

Contents

Quick Start

Installation

Add the Temporalio package from NuGet. For example, using the dotnet CLI:

dotnet add package Temporalio --prerelease

NOTE: This README is for the current branch and not necessarily what's released on NuGet.

Implementing an Activity

Workflow implementation is not yet supported in the .NET SDK, but defining a workflow and implementing activities is.

For example, if you have a SayHelloWorkflow workflow in another Temporal language that invokes SayHello activity on my-activity-queue in C#, you can have the following in a worker project's Program.cs:

using Temporalio;
using Temporalio.Activity;
using Temporalio.Client;
using Temporalio.Worker;
using Temporalio.Workflow;

// Create client
var client = await TemporalClient.ConnectAsync(new ()
{
    TargetHost = "localhost:7233",
    Namespace = "my-namespace",
});

// Create worker
using var worker = new TemporalWorker(client, new ()
{
    TaskQueue = "my-activity-queue",
    Activities = { SayHello },
});

// Run worker until Ctrl+C
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (sender, eventArgs) =>
{
    eventArgs.Cancel = true;
    cts.Cancel();
};
await worker.ExecuteAsync(cts.Token);

// Implementation of an activity in .NET
[Activity]
string SayHello(string name) => $"Hello, {name}!";

// Definition of a workflow implemented in another language
namespace MyNamespace
{
    [Workflow]
    public interface ISayHelloWorkflow
    {
        static readonly ISayHelloWorkflow Ref = Refs.Create<ISayHelloWorkflow>();

        [WorkflowRun]
        Task<string> RunAsync(string name);
    }
}

Running that will start a worker.

Running a Workflow

Then you can run the workflow, referencing that project in another project's Program.cs:

using Temporalio.Client;

// Create client
var client = await TemporalClient.ConnectAsync(new()
{
    TargetHost = "localhost:7233",
    Namespace = "my-namespace",
});

// Run workflow
var result = await client.ExecuteWorkflowAsync(
    MyNamespace.ISayHelloWorkflow.Ref.RunAsync,
    "Temporal",
    new() { ID = "my-workflow-id", TaskQueue = "my-workflow-queue" });

Console.WriteLine($"Workflow result: {result}");

This will output:

Workflow result: Hello, Temporal!

Usage

Client

A client can be created an used to start a workflow. For example:

using Temporalio.Client;

// Create client connected to server at the given address and namespace
var client = await TemporalClient.ConnectAsync(new()
{
    TargetHost = "localhost:7233",
    Namespace = "my-namespace",
});

// Start a workflow
var handle = await client.StartWorkflowAsync(
    IMyWorkflow.Ref.RunAsync,
    "some workflow argument",
    new() { ID = "my-workflow-id", TaskQueue = "my-workflow-queue" });

// Wait for a result
var result = await handle.GetResultAsync();
Console.WriteLine("Result: {0}", result);

Notes about the above code:

  • Temporal clients are not explicitly disposable.
  • To enable TLS, the Tls option can be set to a non-null TlsOptions instance.
  • Instead of StartWorkflowAsync + GetResultAsync above, there is an ExecuteWorkflowAsync extension method that is clearer if the handle is not needed.
  • Non-typesafe forms of StartWorkflowAsync and ExecuteWorkflowAsync exist when there is no workflow definition or the workflow may take more than one argument or some other dynamic need. These simply take string workflow type names and an object array for arguments.
  • The handle above represents a WorkflowHandle which has specific workflow operations on it. For existing workflows, handles can be obtained via client.GetWorkflowHandle.

Data Conversion

Data converters are used to convert raw Temporal payloads to/from actual .NET types. A custom data converter can be set via the DataConverter option when creating a client. Data converters are a combination of payload converters, payload codecs, and failure converters. Payload converters convert .NET values to/from serialized bytes. Payload codecs convert bytes to bytes (e.g. for compression or encryption). Failure converters convert exceptions to/from serialized failures.

Data converters are in the Temporalio.Converters namespace. The default data converter uses a default payload converter, which supports the following types:

  • null
  • byte[]
  • Google.Protobuf.IMessage instances
  • Anything that System.Text.Json supports

Custom converters can be created for all uses. Due to potential sandboxing use, payload converters must be specified as types not instances. For example, to create client with a data converter that converts all C# property names to camel case, you would:

using System.Text.Json;
using Temporalio.Client;
using Temporalio.Converters;

public class CamelCasePayloadConverter : DefaultPayloadConverter
{
    public CamelCasePayloadConverter()
      : base(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
    {
    }
}

var client = await TemporalClient.ConnectAsync(new()
{
    TargetHost = "localhost:7233",
    Namespace = "my-namespace",
    DataConverter = DataConverter.Default with { PayloadConverterType = typeof(CamelCasePayloadConverter) },
});

Workers

Workers host workflows and/or activities. Workflows cannot yet be written in .NET, but activities can. Here's how to run an activity worker:

using Temporalio.Client;
using Temporalio.Worker;
using MyNamespace;

// Create client
var client = await TemporalClient.ConnectAsync(new ()
{
    TargetHost = "localhost:7233",
    Namespace = "my-namespace",
});

// Create worker
using var worker = new TemporalWorker(client, new ()
{
    TaskQueue = "my-activity-queue",
    Activities = { MyActivities.MyActivity },
});

// Run worker until Ctrl+C
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (sender, eventArgs) =>
{
    eventArgs.Cancel = true;
    cts.Cancel();
};
await worker.ExecuteAsync(cts.Token);

Notes about the above code:

  • This shows how to run a worker from C# using top-level statements. Of course this can be part of a larger program and ExecuteAsync can be used like any other task call with a cancellation token.
  • The worker uses the same client that is used for all other Temporal tasks (e.g. starting workflows).
  • Workers can have many more options not shown here (e.g. data converters and interceptors).

Workflows

Workflows cannot yet be written in .NET but they can be defined.

Workflow Definition

Workflows are defined as classes or interfaces with a [Workflow] attribute. The entry point method for a workflow has the [WorkflowRun] attribute. Methods for signals and queries have the [WorkflowSignal] and [WorkflowQuery] attributes respectively. Here is an example of a workflow definition:

using System.Threading.Tasks;
using Temporalio.Workflow;

public record GreetingInfo(string Salutation = "Hello", string Name = "<unknown>");

[Workflow]
public interface IGreetingWorkflow
{
    static readonly IGreetingWorkflow Ref = Refs.Create<IGreetingWorkflow>();

    [WorkflowRun]
    Task<string> RunAsync(GreetingInfo initialInfo);

    [WorkflowSignal]
    Task UpdateSalutation(string salutation);

    [WorkflowSignal]
    Task CompleteWithGreeting();

    [WorkflowQuery]
    string CurrentGreeting();
}

Notes about the above code:

  • The workflow client needs the ability to reference these instance methods, but C# doesn't allow referencing instance methods without an instance. Therefore we add a readonly Ref instance which is a proxy instance just for method references.
    • This is backed by a dynamic proxy generator but method invocations should never be made on it. It is only for referencing methods.
    • This is technically not needed. Any way that the method can be referenced for a client is acceptable.
    • Source generators will provide an additional, alternative way to use workflows in a typed way in the future.
  • [Workflow] attribute must be present on the workflow type.
    • The attribute can have a string argument for the workflow type name. Otherwise the name is defaulted to the unqualified type name (with the I prefix removed if on an interface and has a capital letter following).
  • [WorkflowRun] attribute must be present on one and only one public method.
    • The workflow run method must return a Task or Task<>.
    • The workflow run method should accept a single parameter and return a single type. Records are encouraged because optional fields can be added without affecting backwards compatibility of the workflow definition.
    • The parameters of this method and its return type are considered the parameters and return type of the workflow itself.
    • This attribute is not inherited and this method must be explicitly defined on the declared workflow type. Even if the method functionality should be inherited, for clarity reasons it must still be explicitly defined even if it just invokes the base class method.
  • [WorkflowSignal] attribute may be present on any public method that handles signals.
    • Signal methods must return a Task.
    • The attribute can have a string argument for the signal name. Otherwise the name is defaulted to the unqualified method name with Async trimmed off the end if it is present.
    • This attribute is not inherited and therefore must be explicitly set on any override.
  • [WorkflowQuery] attribute may be present on any public method that handles queries.
    • Query methods must be non-void but cannot return a Task (i.e. they cannot be async).
    • The attribute can have a string argument for the query name. Otherwise the name is defaulted to the unqualified method name.
    • This attribute is not inherited and therefore must be explicitly set on any override.

Activities

Activity Definition

Activities are methods with the [Activity] annotation like so:

namespace MyNamespace;

using System.Net.Http;
using System.Threading.Tasks;
using System.Timers;
using Temporalio.Activity;

public static class Activities
{
    private static readonly HttpClient client = new();

    [Activity]
    public static async Task<string> GetPageAsync(string url)
    {
        // Heartbeat every 2s
        using var timer = new Timer(2000)
        {
            AutoReset = true,
            Enabled = true,
        };
        timer.Elapsed += (sender, eventArgs) => ActivityContext.Current.Heartbeat();

        // Issue our HTTP call
        using var response = await client.GetAsync(url, ActivityContext.Current.CancellationToken);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync(ActivityContext.Current.CancellationToken);
    }
}

Notes about activity definitions:

  • All activities must have the [Activity] attribute.
  • [Activity] can be given a custom string name.
    • If unset, the default is the method's unqualified name. If the method name ends with Async and returns a Task, the default name will have Async trimmed off the end.
  • Long running activities should heartbeat to regularly to inform server the activity is still running.
    • Heartbeats are throttled internally, so users can call this frequently without fear of calling too much.
    • Activities must heartbeat to receive cancellation.
  • Activities can be defined on static as instance methods. They can even be lambdas or local methods, but rarely is this valuable since often an activity will be referenced by a workflow.
  • Activities can be synchronous or asynchronous. If an activity returns a Task, that task is awaited on as part of the activity.
Activity Context

During activity execution, an async-local activity context is available via ActivityContext.Current. This will throw if not currently in an activity context (which can be checked with ActivityContext.HasCurrent). It contains the following important members:

  • Info - Information about the activity.
  • Logger - A logger scoped to the activity.
  • CancelReason - If CancellationToken is cancelled, this will contain the reason.
  • CancellationToken - Token cancelled when the activity is cancelled.
  • Hearbeat(object?...) - Send a heartbeat from this activity.
  • WorkerShutdownToken - Token cancelled on worker shutdown before the grace period + CancellationToken cancellation.
Activity Heartbeating and Cancellation

In order for a non-local activity to be notified of cancellation requests, it must invoke ActivityContext.Heartbeat(). It is strongly recommended that all but the fastest executing activities call this function regularly.

In addition to obtaining cancellation information, heartbeats also support detail data that is persisted on the server for retrieval during activity retry. If an activity calls ActivityContext.Heartbeat(123) and then fails and is retried, ActivityContext.Info.HeartbeatDetails will contain the last detail payloads. A helper can be used to convert, so await ActivityContext.Info.HeartbeatDetailAtAsync<int>(0) would give 123 on the next attempt.

Heartbeating has no effect on local activities.

Activity Worker Shutdown

An activity can react to a worker shutdown specifically.

Upon worker shutdown, ActivityContext.WorkerShutdownToken is cancelled. Then the worker will wait a grace period set by the GracefulShutdownTimeout worker option (default as 0) before issuing actual cancellation to all still-running activities via ActivityContext.CancellationToken.

Worker shutdown will wait on all activities to complete, so if a long-running activity does not respect cancellation, the shutdown may never complete.

Activity Testing

Unit testing an activity or any code that could run in an activity is done via the Temporalio.Testing.ActivityEnvironment class. Simply instantiate the class, and any function passed to RunAsync will be invoked inside the activity context. The following important members are available on the environment to affect the activity context:

  • Info - Activity info, defaulted to a basic set of values.
  • Logger - Activity logger, defaulted to a null logger.
  • Cancel(CancelReason) - Helper to set the reason and cancel the source.
  • CancelReason - Cancel reason.
  • CancellationTokenSource - Token source for issuing cancellation.
  • Heartbeater - Callback invoked each heartbeat.
  • WorkerShutdownTokenSource - Token source for issuing worker shutdown.

Development

Build

Prerequisites:

With all prerequisites in place, run:

dotnet build

Or for release:

dotnet build --configuration Release

Code formatting

This project uses StyleCop analyzers with some overrides in .editorconfig. To format, run:

dotnet format

Can also run with --verify-no-changes to ensure it is formatted.

VisualStudio Code

When developing in vscode, the following JSON settings will enable StyleCop analyzers:

    "omnisharp.enableEditorConfigSupport": true,
    "omnisharp.enableRoslynAnalyzers": true

Testing

Run:

dotnet test

Can add options like:

  • --logger "console;verbosity=detailed" to show logs
  • --filter "FullyQualifiedName=Temporalio.Tests.Client.TemporalClientTests.ConnectAsync_Connection_Succeeds" to run a specific test
  • --blame-crash to do a host process dump on crash

To help debug native pieces and show full stdout/stderr, this is also available as an in-proc test program. Run:

dotnet run --project tests/Temporalio.Tests

Extra args can be added after --, e.g. -- -verbose would show verbose logs and -- --help would show other options. If the arguments are anything but --help, the current assembly is prepended to the args before sending to the xUnit runner.

Rebuilding Rust extension and interop layer

To regen core interop from header, install ClangSharpPInvokeGenerator like:

dotnet tool install --global ClangSharpPInvokeGenerator

Then, run:

ClangSharpPInvokeGenerator @src/Temporalio/Bridge/GenerateInterop.rsp

The Rust DLL is built automatically when the project is built. protoc may need to be on the PATH to build the Rust DLL.

Regenerating protos

Must have protoc on the PATH. Note, for now you must use protoc 3.x until our GH action downloader is fixed or we change how we download protoc and check protos (since protobuf changed some C# source).

Then:

dotnet run --project src/Temporalio.Api.Generator

Regenerating API docs

Install docfx, then run:

docfx src/Temporalio.ApiDoc/docfx.json
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 is compatible. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on Temporalio:

Package Downloads
Temporalio.Extensions.Hosting The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org.

Temporal SDK .NET Hosting and Dependency Injection Extension

Temporalio.Extensions.OpenTelemetry The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org.

Temporal SDK .NET OpenTelemetry Extension

Neon.Temporal

Useful classes and extensions for Temporal

Temporalio.Extensions.DiagnosticSource The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org.

Temporal SDK .NET Diagnostic Source Extension

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 53,515 12/5/2023
0.1.0-beta2 15,863 10/30/2023
0.1.0-beta1 25,007 7/24/2023
0.1.0-alpha6 5,267 6/29/2023
0.1.0-alpha5 7,764 5/26/2023
0.1.0-alpha4 2,286 5/1/2023
0.1.0-alpha3 1,008 4/20/2023
0.1.0-alpha2 1,769 2/10/2023
0.1.0-alpha1 703 2/2/2023