Temporalio 0.1.0-alpha2
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
<PackageReference Include="Temporalio" Version="0.1.0-alpha2" />
paket add Temporalio --version 0.1.0-alpha2
#r "nuget: Temporalio, 0.1.0-alpha2"
// 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
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:
- NuGet Package
- Application Development Guide (TODO: .NET docs)
- API Documentation
- Samples
⚠️ 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-nullTlsOptions
instance. - Instead of
StartWorkflowAsync
+GetResultAsync
above, there is anExecuteWorkflowAsync
extension method that is clearer if the handle is not needed. - Non-typesafe forms of
StartWorkflowAsync
andExecuteWorkflowAsync
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 aWorkflowHandle
which has specific workflow operations on it. For existing workflows, handles can be obtained viaclient.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).
- The attribute can have a string argument for the workflow type name. Otherwise the name is defaulted to the
unqualified type name (with the
[WorkflowRun]
attribute must be present on one and only one public method.- The workflow run method must return a
Task
orTask<>
. - 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.
- The workflow run method must return a
[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.
- Signal methods must return a
[WorkflowQuery]
attribute may be present on any public method that handles queries.- Query methods must be non-
void
but cannot return aTask
(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.
- Query methods must be non-
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 aTask
, the default name will haveAsync
trimmed off the end.
- If unset, the default is the method's unqualified name. If the method name ends with
- 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
- IfCancellationToken
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:
- .NET
- Rust (i.e.
cargo
on thePATH
) - Protobuf Compiler (i.e.
protoc
on thePATH
) - This repository, cloned recursively
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 | Versions 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. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.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. |
-
.NETCoreApp 3.1
- Castle.Core (>= 5.1.0)
- Google.Protobuf (>= 3.21.9)
- Microsoft.Extensions.Logging.Abstractions (>= 2.2.0)
-
.NETFramework 4.6.2
- Castle.Core (>= 5.1.0)
- Google.Protobuf (>= 3.21.9)
- Microsoft.Extensions.Logging.Abstractions (>= 2.2.0)
- System.Text.Json (>= 6.0.7)
-
.NETStandard 2.0
- Castle.Core (>= 5.1.0)
- Google.Protobuf (>= 3.21.9)
- Microsoft.Extensions.Logging.Abstractions (>= 2.2.0)
- System.Text.Json (>= 6.0.7)
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.4.0 | 8 | 12/19/2024 |
1.3.1 | 8 | 9/11/2024 |
1.3.0 | 9 | 8/14/2024 |
1.2.0 | 9 | 6/27/2024 |
1.1.2 | 9 | 6/4/2024 |
1.1.1 | 1 | 5/10/2024 |
1.1.0 | 26 | 5/7/2024 |
1.0.0 | 1 | 12/5/2023 |
0.1.0-beta2 | 1 | 10/30/2023 |
0.1.0-beta1 | 1 | 7/24/2023 |
0.1.0-alpha6 | 1 | 6/28/2023 |
0.1.0-alpha5 | 1 | 5/26/2023 |
0.1.0-alpha4 | 1 | 5/1/2023 |
0.1.0-alpha3 | 1 | 4/20/2023 |
0.1.0-alpha2 | 1 | 2/10/2023 |
0.1.0-alpha1 | 1 | 1/31/2023 |