Temporal .NET SDK

repository·main·Indexed 20 days ago

https://github.com/temporalio/sdk-dotnet

A framework for authoring distributed, scalable, and resilient workflows and activities using .NET. It supports .NET Framework >= 4.6.2, .NET Core >= 3.1 (including .NET 5+), and .NET Standard >= 2.0. The SDK provides tools for defining workflows and activities via attributes, managing workers, and interacting with the Temporal server through a type-safe TemporalClient.

Tokens
31.6K
Snippets
58
Records
96
Agent score
68%

What's inside Temporal .NET SDK

  1. Explore the Temporal .NET SDK namespaces

    main

    The Temporal .NET SDK is organized into several namespaces based on the component you are developing or using:

    Core SDK Namespaces

    • Temporalio.Activities: Types for developing activities.
    • Temporalio.Client: Client for accessing Temporal (starting workflows, querying, etc.).
    • Temporalio.Common: Common types used across other namespaces.
    • Temporalio.Converters: Data converters, including support for custom conversion logic.
    • Temporalio.Exceptions: Well-known exceptions for clients, activities, and workflows.
    • Temporalio.Runtime: Overarching runtime that can have telemetry configured.
    • Temporalio.Testing: Testing environments and utilities for unit and integration testing.
    • Temporalio.Worker: Worker for running workflows and/or activities.
    • Temporalio.Workflows: Types for developing workflows.

    Extension Namespaces

    • Temporalio.Extensions.Aws.Lambda: Support for running Temporal in AWS Lambda.
    • Temporalio.Extensions.Aws.Lambda.OpenTelemetry: OpenTelemetry support specifically for AWS Lambda extensions.
    • Temporalio.Extensions.DiagnosticSource: Integration with .NET DiagnosticSource.
    • Temporalio.Extensions.Hosting: Integration with .NET Generic Host (e.g., IHostedService).
    • Temporalio.Extensions.OpenTelemetry: General OpenTelemetry integration for telemetry and observability.
  2. Define and configure Activities

    main

    Activities are the building blocks for side effects in Temporal.

    • Attributes: All activities must be decorated with the [Activity] attribute. You can provide a custom string name; otherwise, the default is the method's unqualified name (with Async trimmed if it returns a Task).
    • Types: Activities can be static or instance methods, synchronous or asynchronous. They can also be lambdas or local methods.
    • Dynamic Activities: Using [Activity(Dynamic = true)] allows an activity to be called when no other activities match. It must accept a single parameter of type Temporalio.Converters.IRawValue[]. Only one dynamic activity can be registered per worker.
    • Dependency Injection: To use a DI container for activity instantiation, use the Temporalio.Extensions.Hosting project.
  3. Use Shutdown Hooks and manage ShutdownDeadlineBuffer

    main

    The worker runs until the Lambda remaining time reaches the ShutdownDeadlineBuffer. After this point, the worker stops and begins the shutdown process, executing any registered shutdown hooks.

    • ShutdownDeadlineBuffer: A TimeSpan that defines how much time before the Lambda deadline the worker should start shutting down. Increase this if you need more time for cleanup or telemetry flushing.
    • AddShutdownHook: Registers a callback to run after the worker has stopped. Hooks added in the async configure callback are scoped to that specific invocation. Hook failures are logged to the Lambda context logger, but subsequent hooks will still run.
    // Set the buffer
    options.ShutdownDeadlineBuffer = TimeSpan.FromSeconds(15);
    
    // Add a hook for cleanup
    options.AddShutdownHook(async cancellationToken =>
    {
        await FlushTelemetryAsync(cancellationToken);
    });
  4. Workflow Determinism and Logic Constraints

    main

    Temporal Workflows must be deterministic. Because workflows are replayed to recover state, any non-deterministic action will cause the workflow to fail.

    Prohibited actions include:

    • Performing I/O (network, disk, stdio, etc.).
    • Accessing or altering external mutable state.
    • Using standard .NET threading.
    • Using the system clock (e.g., DateTime.Now) or .NET timers (e.g., Task.Delay, Thread.Sleep).
    • Making standard random calls.
    • Using non-deterministic calls (e.g., iterating over a Dictionary where order isn't guaranteed).

    To ensure determinism, always use the provided Workflow utility methods (e.g., Workflow.UtcNow, Workflow.DelayAsync, Workflow.Random) instead of standard .NET equivalents.

  5. How to handle exceptions in Workflows

    main

    Workflows handle exceptions differently depending on the type:

    1. Failing a Workflow/Update explicitly: Throw Temporalio.Exceptions.ApplicationFailureException. You can mark this as non-retryable or include specific details. By default, instances of Temporalio.Exceptions.FailureException will also fail the workflow/update.
    2. Failing a Workflow Task (Retrying): If an exception is thrown that is not an ApplicationFailureException (or a FailureException), the workflow task fails. This causes the workflow to suspend and continually retry until the code is fixed (e.g., due to a bug or non-deterministic error).
    3. Non-deterministic exceptions: These are detected internally and fail the workflow task by default.

    Customizing behavior: You can customize which exceptions turn a workflow task failure into a workflow/update failure using TemporalWorkerOptions.WorkflowFailureExceptionTypes (at the worker level) or FailureExceptionTypes on the WorkflowAttribute (per workflow).

  6. Handle Activity Worker Shutdown

    main

    Activities can react to a worker shutting down via the ActivityExecutionContext.WorkerShutdownToken.

    Shutdown Lifecycle:

    1. The WorkerShutdownToken is cancelled.
    2. The worker waits for a grace period defined by the GracefulShutdownTimeout worker option (default is 0).
    3. After the grace period, the worker issues actual cancellation to all running activities via ActivityExecutionContext.CancellationToken.

    Warning: If an activity does not respect cancellation, the worker shutdown may never complete.

  7. Define a Workflow using attributes

    main

    To define a workflow in the .NET SDK, you must use specific attributes on your class and methods. Attributes are not inherited; if you override a method from a base class or interface, you must re-apply the attribute to the override.

    Key Attributes:

    • [Workflow]: Must be present on the workflow type.
      • You can provide a string argument for the workflow name. If omitted, it defaults to the unqualified type name (removing the I prefix from interfaces).
      • Dynamic = true: Enables a dynamic workflow that is called when no other workflows match. The run call must accept Temporalio.Converters.IRawValue[].
    • [WorkflowRun]: Must be present on exactly one public method. This method defines the entry point.
      • Must return a Task or Task<T>.
      • It is recommended to accept and return a single parameter/type (e.g., a Record) to maintain backward compatibility.
    • [WorkflowSignal]: Used on public methods that handle signals.
      • Must return a Task.
      • Dynamic = true allows a dynamic signal that accepts a string name and Temporalio.Converters.IRawValue[] arguments.
    • [WorkflowQuery]: Used on public methods or properties with a public getter.
      • Must be non-void and cannot be async (cannot return a Task).
      • Dynamic = true allows a dynamic query accepting a string name and Temporalio.Converters.IRawValue[].
    • [WorkflowUpdate]: Used on public methods that handle updates.
      • Must return a Task or Task<T>.
      • Dynamic = true allows a dynamic update accepting a string name and Temporalio.Converters.IRawValue[].
      • Validation: You can create a validator by marking a void method with [WorkflowUpdateValidator(nameof(MyUpdateMethod))]. It must accept the same parameters as the update method. If the validator throws an exception, the update fails without being stored in history.
  8. How Workflow Tracing works with OpenTelemetry

    main

    Tracing within Temporal workflows is complex because workflows are distributed, interruptible, and deterministic. Standard .NET diagnostic activities cannot be resumed across process boundaries or rehydrated during replay, which breaks traditional distributed tracing.

    To solve this, Temporalio.Extensions.OpenTelemetry uses a wrapper called WorkflowActivity. This wrapper provides an implicit parent context for diagnostic activities within a workflow without requiring a long-running, non-deterministic .NET diagnostic activity.

    Key behaviors:

    • Non-Replay Execution: Diagnostic activities for inbound calls (workflow start, signals, queries, updates) and outbound calls (scheduling activities, child workflows, etc.) are created only when not replaying.
    • Parentage: When not replaying, activities are properly parented. During replay, activities are parented to the outer diagnostic activity created on the client outbound call.
    • Immediate Lifecycle: To maintain determinism, WorkflowActivity wrappers start and stop immediately. They exist primarily to hold the context in AsyncLocal so that subsequent activities can be parented correctly.

    ⚠️ WARNING: Do not use the standard .NET diagnostic activity API directly inside workflows. They are non-deterministic and can cause unpredictable behavior during replay.

  9. How Data Converters work

    main

    Data converters translate raw Temporal payloads to and from .NET types. A DataConverter is a combination of:

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

    The default payload converter supports:

    • null
    • byte[]
    • Google.Protobuf.IMessage instances
    • Any type supported by System.Text.Json
    • IRawValue for unconverted raw payloads.
    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 { PayloadConverter = new CamelCasePayloadConverter() },
    });
  10. How workflow inheritance works

    main

    Workflows can inherit from interfaces and base classes, but the SDK uses an explicit non-inheritance strategy for attributes to avoid diamond problems and ensure clarity.

    • [Workflow] and [WorkflowRun]: These are never inherited. Even if a base class or interface defines them, the final implementing class must explicitly define them and the [WorkflowRun] method. If a base class has a [WorkflowRun] implementation, the subclass must override it, apply the [WorkflowRun] attribute to the override, and then call the base method.
    • [WorkflowSignal], [WorkflowQuery], and [WorkflowUpdate]: These can be inherited if the method is not overridden. However, if you declare an override in the subclass, you must explicitly re-apply the attribute to that override.
  11. Define a Workflow with attributes

    main

    Workflows are defined using classes or interfaces decorated with specific attributes:

    • [Workflow]: Marks the class/interface as a workflow.
    • [WorkflowRun]: Marks the entry point method.
    • [WorkflowSignal]: Marks a method used for signals.
    • [WorkflowQuery]: Marks a method used for queries.
    • [WorkflowUpdate]: Marks a method for workflow updates (experimental).

    Important: Workflow code must be deterministic. Use Workflow.ExecuteActivityAsync to call activities and Workflow.WaitConditionAsync for waiting on state changes.

    using Microsoft.Extensions.Logging;
    using Temporalio.Workflow;
    
    public record GreetingParams(string Salutation = "Hello", string Name = "<unknown>");
    
    [Workflow]
    public class GreetingWorkflow
    {
        private string? currentGreeting;
        private GreetingParams? greetingParamsUpdate;
        private bool complete;
    
    [WorkflowRun]
        public async Task<string> RunAsync(GreetingParams initialParams)
        {
            var greetingParams = initialParams;
            while (true)
            {
                currentGreeting = await Workflow.ExecuteActivityAsync(
                    () => GreetingActivities.CreateGreeting(greetingParams),
                    new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
                
                var waitUpdate = Workflow.WaitConditionAsync(() => greetingParamsUpdate != null);
                var waitComplete = Workflow.WaitConditionAsync(() => complete);
                if (waitComplete == await Task.WhenAny(waitUpdate, waitComplete))
                {
                    return currentGreeting!;
                }
                greetingParams = greetingParamsUpdate!;
                greetingParamsUpdate = null;
            }
        }
    
        [WorkflowUpdate]
        public async Task UpdateGreetingParamsAsync(GreetingParams greetingParams) =>
            this.greetingParamsUpdate = greetingParams;
    
        [WorkflowSignal]
        public async Task CompleteWithGreetingAsync() => this.complete = true;
    
        [WorkflowQuery]
        public string CurrentGreeting() => currentGreeting!;
    }
  12. How metrics are recorded with DiagnosticSource extension

    main

    When a TemporalRuntime is configured with a CustomMetricMeter (wrapping a .NET Meter), metrics are recorded across different scopes:

    1. Client Metrics: All metrics generated by the TemporalClient are sent to the provided .NET meter.
    2. Worker Metrics: The client can be used to create a Worker, ensuring worker-level metrics are captured.
    3. Activity Metrics: Metrics created during activity execution using ActivityExecutionContext.Current.MetricMeter are recorded on the .NET meter.
    4. Workflow Metrics: Metrics created during workflow execution using Workflow.MetricMeter are recorded on the .NET meter.