Files
temporalio__skill-temporal-…/references/dotnet/advanced-features.md

6.6 KiB

.NET SDK Advanced Features

Schedules

Create recurring workflow executions.

using Temporalio.Client.Schedules;

var scheduleId = "daily-report";
await client.CreateScheduleAsync(
    scheduleId,
    new Schedule(
        Action: ScheduleActionStartWorkflow.Create(
            (DailyReportWorkflow wf) => wf.RunAsync(),
            new(id: "daily-report", taskQueue: "reports")),
        Spec: new ScheduleSpec
        {
            Intervals = new List<ScheduleIntervalSpec>
            {
                new(Every: TimeSpan.FromDays(1)),
            },
        }));

// Manage schedules
var handle = client.GetScheduleHandle(scheduleId);
await handle.PauseAsync("Maintenance window");
await handle.UnpauseAsync();
await handle.TriggerAsync(); // Run immediately
await handle.DeleteAsync();

Async Activity Completion

For activities that complete asynchronously (e.g., human tasks, external callbacks). If you configure a HeartbeatTimeout on this activity, the external completer is responsible for sending heartbeats via the async handle. If you do NOT set a HeartbeatTimeout, no heartbeats are required.

Note: If the external system that completes the asynchronous action can reliably be trusted to do the task and Signal back with the result, and it doesn't need to Heartbeat or receive Cancellation, then consider using signals instead.

using Temporalio.Activities;
using Temporalio.Client;

[Activity]
public async Task RequestApprovalAsync(string requestId)
{
    var taskToken = ActivityExecutionContext.Current.Info.TaskToken;

    // Store task token for later completion (e.g., in database)
    await StoreTaskTokenAsync(requestId, taskToken);

    // Mark this activity as waiting for external completion
    throw new CompleteAsyncException();
}

// Later, complete the activity from another process
public async Task CompleteApprovalAsync(string requestId, bool approved)
{
    var client = await TemporalClient.ConnectAsync(new("localhost:7233"));
    // Retrieve the task token from external storage (e.g., database)
    var taskToken = await GetTaskTokenAsync(requestId);

    var handle = client.GetAsyncActivityHandle(taskToken);

    // Optional: if a HeartbeatTimeout was set, you can periodically:
    // await handle.HeartbeatAsync(progressDetails);

    if (approved)
        await handle.CompleteAsync("approved");
    else
        // You can also fail or report cancellation via the handle
        await handle.FailAsync(new ApplicationFailureException("Rejected"));
}

Worker Tuning

Configure worker performance settings.

var worker = new TemporalWorker(
    client,
    new TemporalWorkerOptions("my-task-queue")
    {
        // Workflow task concurrency
        MaxConcurrentWorkflowTasks = 100,
        // Activity task concurrency
        MaxConcurrentActivities = 100,
        // Graceful shutdown timeout
        GracefulShutdownTimeout = TimeSpan.FromSeconds(30),
    }
    .AddWorkflow<MyWorkflow>()
    .AddAllActivities(new MyActivities()));

Workflow Init Attribute

You should always put state initialization logic in the constructor of your workflow class, so that it happens before signals/updates arrive.

Normally, your constructor must have no arguments. However, if you add the [WorkflowInit] attribute, then your constructor instead receives the same workflow arguments that [WorkflowRun] receives:

[Workflow]
public class MyWorkflow
{
    private readonly string _initialValue;
    private readonly List<string> _items = new();

    [WorkflowInit]
    public MyWorkflow(string initialValue)
    {
        _initialValue = initialValue;
    }

    [WorkflowRun]
    public async Task<string> RunAsync(string initialValue)
    {
        // _initialValue and _items are already initialized
        return _initialValue;
    }
}

Constructor (with [WorkflowInit]) and [WorkflowRun] method must have the same parameters with the same types. You cannot make blocking calls (activities, sleeps, etc.) from the constructor.

Workflow Failure Exception Types

Control which exceptions cause workflow failures vs workflow task retries.

Default behavior: Only ApplicationFailureException fails a workflow. All other exceptions retry the workflow task forever (treated as bugs to fix with a code deployment).

Tip for testing: Set WorkflowFailureExceptionTypes to include Exception so any unhandled exception fails the workflow immediately rather than retrying the workflow task forever. This surfaces bugs faster.

Worker-Level Configuration

var worker = new TemporalWorker(
    client,
    new TemporalWorkerOptions("my-task-queue")
    {
        // These exception types will fail the workflow execution (not just the task)
        WorkflowFailureExceptionTypes = new[] { typeof(ArgumentException), typeof(InvalidOperationException) },
    }
    .AddWorkflow<MyWorkflow>()
    .AddAllActivities(new MyActivities()));

Dependency Injection

The .NET SDK supports dependency injection via the Temporalio.Extensions.Hosting package, which integrates with .NET's generic host.

Worker as Generic Host

using Temporalio.Extensions.Hosting;

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = Host.CreateDefaultBuilder(args)
            .ConfigureServices(ctx =>
                ctx.
                    AddScoped<IOrderRepository, OrderRepository>().
                    AddHostedTemporalWorker(
                        clientTargetHost: "localhost:7233",
                        clientNamespace: "default",
                        taskQueue: "my-task-queue").
                    AddScopedActivities<MyActivities>().
                    AddWorkflow<MyWorkflow>())
            .Build();
        await host.RunAsync();
    }
}

Activity Dependency Injection

As shown in the host setup above, activities can be registered with AddScopedActivities<T>(), AddSingletonActivities<T>(), or AddTransientActivities<T>(). Activities registered this way are created via DI, allowing constructor injection:

public class MyActivities
{
    private readonly ILogger<MyActivities> _logger;
    private readonly IOrderRepository _repository;

    public MyActivities(ILogger<MyActivities> logger, IOrderRepository repository)
    {
        _logger = logger;
        _repository = repository;
    }

    [Activity]
    public async Task<Order> GetOrderAsync(string orderId)
    {
        _logger.LogInformation("Fetching order {OrderId}", orderId);
        return await _repository.GetAsync(orderId);
    }
}

Note: Dependency injection is NOT available in workflows — workflows must be self-contained for determinism.