{"id":13542927,"url":"https://github.com/temporalio/sdk-dotnet","last_synced_at":"2025-04-02T12:30:50.821Z","repository":{"id":65611438,"uuid":"577960478","full_name":"temporalio/sdk-dotnet","owner":"temporalio","description":"Temporal .NET SDK","archived":false,"fork":false,"pushed_at":"2024-05-22T12:54:27.000Z","size":2136,"stargazers_count":344,"open_issues_count":32,"forks_count":27,"subscribers_count":22,"default_branch":"main","last_synced_at":"2024-05-28T11:10:32.636Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/temporalio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-13T23:29:29.000Z","updated_at":"2024-05-28T10:35:10.000Z","dependencies_parsed_at":"2024-04-24T22:29:19.384Z","dependency_job_id":"bc6840bd-b043-45ba-865c-3d40ce4d4345","html_url":"https://github.com/temporalio/sdk-dotnet","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Fsdk-dotnet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Fsdk-dotnet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Fsdk-dotnet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Fsdk-dotnet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/temporalio","download_url":"https://codeload.github.com/temporalio/sdk-dotnet/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246815306,"owners_count":20838420,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T11:00:19.916Z","updated_at":"2025-04-02T12:30:45.813Z","avatar_url":"https://github.com/temporalio.png","language":"C#","funding_links":[],"categories":[".NET","C\\#","C#"],"sub_categories":["Videos"],"readme":"![Temporal .NET SDK](https://raw.githubusercontent.com/temporalio/assets/main/files/w/dotnet.png)\n\n[![NuGet](https://img.shields.io/nuget/vpre/temporalio.svg?style=for-the-badge)](https://www.nuget.org/packages/Temporalio)\n[![MIT](https://img.shields.io/github/license/temporalio/sdk-dotnet.svg?style=for-the-badge)](LICENSE)\n\n[Temporal](https://temporal.io/) is a distributed, scalable, durable, and highly available orchestration engine used to\nexecute asynchronous, long-running business logic in a scalable and resilient way.\n\n\"Temporal .NET SDK\" is the framework for authoring workflows and activities using .NET programming languages.\n\nAlso see:\n\n* [Application Development Guide](https://docs.temporal.io/develop/dotnet/)\n* [.NET Samples](https://github.com/temporalio/samples-dotnet)\n* [API Documentation](https://dotnet.temporal.io/api)\n\nExtensions:\n\n* [Temporalio.Extensions.DiagnosticSource](https://github.com/temporalio/sdk-dotnet/tree/main/src/Temporalio.Extensions.DiagnosticSource) -\n  `System.Diagnostics.Metrics` support for SDK metrics\n* [Temporalio.Extensions.Hosting](https://github.com/temporalio/sdk-dotnet/tree/main/src/Temporalio.Extensions.Hosting) -\n  Client dependency injection, activity dependency injection, and worker generic host support\n* [Temporalio.Extensions.OpenTelemetry](https://github.com/temporalio/sdk-dotnet/tree/main/src/Temporalio.Extensions.OpenTelemetry) -\n  OpenTelemetry tracing support\n\n---\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n**Contents**\n\n- [Quick Start](#quick-start)\n  - [Installation](#installation)\n  - [Implementing a Workflow and Activity](#implementing-a-workflow-and-activity)\n  - [Running a Worker](#running-a-worker)\n  - [Executing a Workflow](#executing-a-workflow)\n- [Usage](#usage)\n  - [Clients](#clients)\n    - [Cloud Client Using MTLS](#cloud-client-using-mtls)\n    - [Client Dependency Injection](#client-dependency-injection)\n    - [Data Conversion](#data-conversion)\n  - [Workers](#workers)\n    - [Worker as Generic Host](#worker-as-generic-host)\n  - [Workflows](#workflows)\n    - [Workflow Definition](#workflow-definition)\n      - [Workflow Inheritance](#workflow-inheritance)\n    - [Running Workflows](#running-workflows)\n    - [Invoking Activities](#invoking-activities)\n    - [Invoking Child Workflows](#invoking-child-workflows)\n    - [Timers and Conditions](#timers-and-conditions)\n    - [Workflow Task Scheduling and Cancellation](#workflow-task-scheduling-and-cancellation)\n    - [Workflow Utilities](#workflow-utilities)\n    - [Workflow Exceptions](#workflow-exceptions)\n    - [Workflow Logic Constraints](#workflow-logic-constraints)\n      - [.NET Task Determinism](#net-task-determinism)\n      - [Workflow .editorconfig](#workflow-editorconfig)\n    - [Workflow Testing](#workflow-testing)\n      - [Automatic Time Skipping](#automatic-time-skipping)\n      - [Manual Time Skipping](#manual-time-skipping)\n      - [Mocking Activities](#mocking-activities)\n    - [Workflow Replay](#workflow-replay)\n  - [Activities](#activities)\n    - [Activity Definition](#activity-definition)\n    - [Activity Dependency Injection](#activity-dependency-injection)\n    - [Activity Execution Context](#activity-execution-context)\n    - [Activity Heartbeating and Cancellation](#activity-heartbeating-and-cancellation)\n    - [Activity Worker Shutdown](#activity-worker-shutdown)\n    - [Activity Testing](#activity-testing)\n  - [OpenTelemetry Tracing Support](#opentelemetry-tracing-support)\n  - [Built-in Native Shared Library](#built-in-native-shared-library)\n- [Development](#development)\n  - [Build](#build)\n  - [Code formatting](#code-formatting)\n    - [VisualStudio Code](#visualstudio-code)\n  - [Testing](#testing)\n  - [Rebuilding Rust extension and interop layer](#rebuilding-rust-extension-and-interop-layer)\n  - [Regenerating protos](#regenerating-protos)\n  - [Regenerating API docs](#regenerating-api-docs)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Quick Start\n\n### Installation\n\nAdd the `Temporalio` package from [NuGet](https://www.nuget.org/packages/Temporalio). For example, using the `dotnet`\nCLI:\n\n    dotnet add package Temporalio\n\nIf you are using .NET Framework or a non-standard target platform, see the\n[Built-in Native Shared Library](#built-in-native-shared-library) section later for additional information.\n\n**NOTE: This README is for the current branch and not necessarily what's released on NuGet.**\n\n### Implementing a Workflow and Activity\n\nAssuming the [ImplicitUsings](https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#implicitusings) is\nenabled, create an activity by putting the following in `MyActivities.cs`:\n\n```csharp\nnamespace MyNamespace;\n\nusing Temporalio.Activities;\n\npublic class MyActivities\n{\n    // Activities can be async and/or static too! We just demonstrate instance\n    // methods since many will use them that way.\n    [Activity]\n    public string SayHello(string name) =\u003e $\"Hello, {name}!\";\n}\n```\n\nThat creates the activity. Now to create the workflow, put the following in `SayHelloWorkflow.workflow.cs`:\n\n```csharp\nnamespace MyNamespace;\n\nusing Temporalio.Workflows;\n\n[Workflow]\npublic class SayHelloWorkflow\n{\n    [WorkflowRun]\n    public async Task\u003cstring\u003e RunAsync(string name)\n    {\n        // This workflow just runs a simple activity to completion.\n        // StartActivityAsync could be used to just start and there are many\n        // other things that you can do inside a workflow.\n        return await Workflow.ExecuteActivityAsync(\n            // This is a lambda expression where the instance is typed. If this\n            // were static, you wouldn't need a parameter.\n            (MyActivities act) =\u003e act.SayHello(name),\n            new() { ScheduleToCloseTimeout = TimeSpan.FromMinutes(5) });\n    }\n}\n```\n\nThis is a simple workflow that executes the `SayHello` activity.\n\n### Running a Worker\n\nTo run this in a worker, put the following in `Program.cs`:\n\n```csharp\nusing MyNamespace;\nusing Temporalio.Client;\nusing Temporalio.Worker;\n\n// Create a client to localhost on \"default\" namespace\nvar client = await TemporalClient.ConnectAsync(new(\"localhost:7233\"));\n\n// Cancellation token to shutdown worker on ctrl+c\nusing var tokenSource = new CancellationTokenSource();\nConsole.CancelKeyPress += (_, eventArgs) =\u003e\n{\n    tokenSource.Cancel();\n    eventArgs.Cancel = true;\n};\n\n// Create an activity instance since we have instance activities. If we had\n// all static activities, we could just reference those directly.\nvar activities = new MyActivities();\n\n// Create worker with the activity and workflow registered\nusing var worker = new TemporalWorker(\n    client,\n    new TemporalWorkerOptions(\"my-task-queue\").\n        AddActivity(activities.SayHello).\n        AddWorkflow\u003cSayHelloWorkflow\u003e());\n\n// Run worker until cancelled\nConsole.WriteLine(\"Running worker\");\ntry\n{\n    await worker.ExecuteAsync(tokenSource.Token);\n}\ncatch (OperationCanceledException)\n{\n    Console.WriteLine(\"Worker cancelled\");\n}\n```\n\nWhen executed, this will listen for Temporal server requests to perform workflow and activity invocations.\n\n### Executing a Workflow\n\nTo start and wait on a workflow result, with the worker program running elsewhere, put the following in a different\nproject's `Program.cs` that references the worker project:\n\n```csharp\nusing MyNamespace;\nusing Temporalio.Client;\n\n// Create a client to localhost on \"default\" namespace\nvar client = await TemporalClient.ConnectAsync(new(\"localhost:7233\"));\n\n// Run workflow\nvar result = await client.ExecuteWorkflowAsync(\n    (SayHelloWorkflow wf) =\u003e wf.RunAsync(\"Temporal\"),\n    new(id: \"my-workflow-id\", taskQueue: \"my-task-queue\"));\n\nConsole.WriteLine(\"Workflow result: {0}\", result);\n```\n\nThis will output:\n\n    Workflow result: Hello, Temporal!\n\n## Usage\n\n### Clients\n\nA client can be created and used to start a workflow. For example:\n\n```csharp\nusing MyNamespace;\nusing Temporalio.Client;\n\n// Create client connected to server at the given address and namespace\nvar client = await TemporalClient.ConnectAsync(new()\n{\n    TargetHost = \"localhost:7233\",\n    Namespace = \"my-namespace\",\n});\n\n// Start a workflow\nvar handle = await client.StartWorkflowAsync(\n    (MyWorkflow wf) =\u003e wf.RunAsync(\"some workflow argument\"),\n    new() { ID = \"my-workflow-id\", TaskQueue = \"my-task-queue\" });\n\n// Wait for a result\nvar result = await handle.GetResultAsync();\nConsole.WriteLine(\"Result: {0}\", result);\n```\n\nNotes about the above code:\n\n* Temporal clients are not explicitly disposable.\n* To enable TLS, the `Tls` option can be set to a non-null `TlsOptions` instance.\n* Instead of `StartWorkflowAsync` + `GetResultAsync` above, there is an `ExecuteWorkflowAsync` extension method that is\n  clearer if the handle is not needed.\n* The type-safe forms of `StartWorkflowAsync` and `ExecuteWorkflowAsync` accept a lambda expression that take the\n  workflow as the first parameter and must only call the run method in the lambda.\n* Non-type-safe forms of `StartWorkflowAsync` and `ExecuteWorkflowAsync` exist when there is no workflow definition or\n  the workflow may take more than one argument or some other dynamic need. These simply take string workflow type names\n  and an object array for arguments.\n* The `handle` above represents a `WorkflowHandle` which has specific workflow operations on it. For existing workflows,\n  handles can be obtained via `client.GetWorkflowHandle`.\n\n#### Cloud Client Using MTLS\n\nAssuming a client certificate is present at `my-cert.pem` and a client key is present at `my-key.pem`, this is how to\nconnect to Temporal Cloud:\n\n```csharp\nusing Temporalio.Client;\n\nvar client = await TemporalClient.ConnectAsync(new(\"my-namespace.a1b2c.tmprl.cloud:7233\")\n{\n    Namespace = \"my-namespace.a1b2c\",\n    Tls = new()\n    {\n        ClientCert = await File.ReadAllBytesAsync(\"my-cert.pem\"),\n        ClientPrivateKey = await File.ReadAllBytesAsync(\"my-key.pem\"),\n    },\n});\n```\n\n#### Client Dependency Injection\n\nTo create clients for use with dependency injection, see the\n[Temporalio.Extensions.Hosting](https://github.com/temporalio/sdk-dotnet/tree/main/src/Temporalio.Extensions.Hosting/)\nproject which has extension methods for creating singleton clients lazily on service collections.\n\nFor manually creating clients for dependency injection, users may prefer `TemporalClient.CreateLazy` which is a\nsynchronous method which creates a client that does not attempt to connect until the first call is made on it. This can\nbe helpful for dependency injection but beware that deferring connection until later can make it hard to see issues with\nconnection parameters as early as may be expected. However, connection must be made before a worker is created with it.\n\n#### Data Conversion\n\nData converters are used to convert raw Temporal payloads to/from actual .NET types. A custom data converter can be set\nvia the `DataConverter` option when creating a client. Data converters are a combination of payload converters, payload\ncodecs, and failure converters. Payload converters convert .NET values to/from serialized bytes. Payload codecs convert\nbytes to bytes (e.g. for compression or encryption). Failure converters convert exceptions to/from serialized failures.\n\nData converters are in the `Temporalio.Converters` namespace. The default data converter uses a default payload\nconverter, which supports the following types:\n\n* `null`\n* `byte[]`\n* `Google.Protobuf.IMessage` instances\n* Anything that `System.Text.Json` supports\n* `IRawValue` as unconverted raw payloads\n\nCustom converters can be created for all uses. For example, to create client with a data converter that converts all C#\nproperty names to camel case, you would:\n\n```csharp\nusing System.Text.Json;\nusing Temporalio.Client;\nusing Temporalio.Converters;\n\npublic class CamelCasePayloadConverter : DefaultPayloadConverter\n{\n    public CamelCasePayloadConverter()\n      : base(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })\n    {\n    }\n}\n\nvar client = await TemporalClient.ConnectAsync(new()\n{\n    TargetHost = \"localhost:7233\",\n    Namespace = \"my-namespace\",\n    DataConverter = DataConverter.Default with { PayloadConverter = new CamelCasePayloadConverter() },\n});\n```\n\n### Workers\n\nWorkers host workflows and/or activities. Here's how to run a worker:\n\n```csharp\nusing MyNamespace;\nusing Temporalio.Client;\nusing Temporalio.Worker;\n\n// Create client\nvar client = await TemporalClient.ConnectAsync(new ()\n{\n    TargetHost = \"localhost:7233\",\n    Namespace = \"my-namespace\",\n});\n\n// Create worker\nusing var worker = new TemporalWorker(\n    client,\n    new TemporalWorkerOptions(\"my-task-queue\").\n        AddActivity(MyActivities.MyActivity).\n        AddWorkflow\u003cMyWorkflow\u003e());\n\n// Run worker until Ctrl+C\nusing var cts = new CancellationTokenSource();\nConsole.CancelKeyPress += (sender, eventArgs) =\u003e\n{\n    eventArgs.Cancel = true;\n    cts.Cancel();\n};\nawait worker.ExecuteAsync(cts.Token);\n```\n\nNotes about the above code:\n\n* This shows how to run a worker from C# using top-level statements. Of course this can be part of a larger program and\n  `ExecuteAsync` can be used like any other task call with a cancellation token.\n* The worker uses the same client that is used for all other Temporal tasks (e.g. starting workflows).\n* Workers can have many more options not shown here (e.g. data converters and interceptors).\n\n#### Worker as Generic Host\n\nSee the\n[Temporalio.Extensions.Hosting](https://github.com/temporalio/sdk-dotnet/tree/main/src/Temporalio.Extensions.Hosting/)\nproject for support for worker services and client/activity dependency injection.\n\n### Workflows\n\n#### Workflow Definition\n\nWorkflows are defined as classes or interfaces with a `[Workflow]` attribute. The entry point method for a workflow has\nthe `[WorkflowRun]` attribute. Methods for signals and queries have the `[WorkflowSignal]` and `[WorkflowQuery]`\nattributes respectively. Here is an example of a workflow definition:\n\n```csharp\nusing Microsoft.Extensions.Logging;\nusing Temporalio.Workflow;\n\npublic record GreetingParams(string Salutation = \"Hello\", string Name = \"\u003cunknown\u003e\");\n\n[Workflow]\npublic class GreetingWorkflow\n{\n    private string? currentGreeting;\n    private GreetingParams? greetingParamsUpdate;\n    private bool complete;\n\n    [WorkflowRun]\n    public async Task\u003cstring\u003e RunAsync(GreetingParams initialParams)\n    {\n        var greetingParams = initialParams;\n        while (true)\n        {\n            // Call activity to create greeting and store as field\n            currentGreeting = await Workflow.ExecuteActivityAsync(\n                // This is a static activity method. If it were an instance\n                // method, a typed parameter can be accepted in the lambda call.\n                () =\u003e GreetingActivities.CreateGreeting(greetingParams),\n                new() { ScheduleToCloseTimeout = TimeSpan.FromMinutes(5) });\n            Workflow.Logger.LogDebug(\"Greeting set to {Greeting}\", currentGreeting);\n\n            // Wait for param update or complete signal. Note, cancellation can\n            // occur by default on WaitConditionAsync calls so cancellation\n            // token does not need to be passed explicitly.\n            var waitUpdate = Workflow.WaitConditionAsync(() =\u003e greetingParamsUpdate != null);\n            var waitComplete = Workflow.WaitConditionAsync(() =\u003e complete);\n            if (waitComplete == await Task.WhenAny(waitUpdate, waitComplete))\n            {\n                // Just return the greeting\n                return currentGreeting!;\n            }\n            // We know it was an update, so update it and continue\n            greetingParams = greetingParamsUpdate!;\n            greetingParamsUpdate = null;\n        }\n    }\n\n    // WARNING: Workflow updates are experimental\n    [WorkflowUpdate]\n    public async Task UpdateGreetingParamsAsync(GreetingParams greetingParams) =\u003e\n      this.greetingParamsUpdate = greetingParams;\n\n    [WorkflowSignal]\n    public async Task CompleteWithGreetingAsync() =\u003e this.complete = true;\n\n    [WorkflowQuery]\n    public string CurrentGreeting() =\u003e currentGreeting;\n}\n```\n\nNotes about the above code:\n\n* Interfaces and abstract methods can have these attributes. This is helpful for defining a workflow implemented\n  elsewhere. But if/when implemented, all pieces of the implementation should have the attributes too.\n* This workflow continually updates the greeting params when signalled and can complete with the greeting when given a\n  different signal\n* Workflow code must be deterministic. See the \"Workflow Logic Constraints\" section below.\n* `Workflow.ExecuteActivityAsync` is strongly typed and accepts a lambda expression. This activity call can be a sync or\n  async function, return a value or not, and invoked statically or on an instance (which would require accepting the\n  instance as the only lambda parameter).\n\nAttributes that can be applied:\n\n* `[Workflow]` attribute must be present on the workflow type.\n  * The attribute can have a string argument for the workflow type name. Otherwise the name is defaulted to the\n    unqualified type name (with the `I` prefix removed if on an interface and has a capital letter following).\n  * `Dynamic = true` can be set for the workflow which makes the workflow a dynamic workflow meaning it will be called\n    when no other workflows match. The run call must accept a single parameter of `Temporalio.Converters.IRawValue[]`\n    for the arguments. Only one dynamic workflow may be registered on a worker.\n* `[WorkflowRun]` attribute must be present on one and only one public method.\n  * The workflow run method must return a `Task` or `Task\u003c\u003e`.\n  * The workflow run method _should_ accept a single parameter and return a single type. Records are encouraged because\n    optional fields can be added without affecting backwards compatibility of the workflow definition.\n  * The parameters of this method and its return type are considered the parameters and return type of the workflow\n    itself.\n  * This attribute is not inherited and this method must be explicitly defined on the declared workflow type. Even if\n    the method functionality should be inherited, for clarity reasons it must still be explicitly defined even if it\n    just invokes the base class method.\n* `[WorkflowSignal]` attribute may be present on any public method that handles signals.\n  * Signal methods must return a `Task`.\n  * The attribute can have a string argument for the signal name. Otherwise the name is defaulted to the unqualified\n    method name with `Async` trimmed off the end if it is present.\n  * This attribute is not inherited and therefore must be explicitly set on any override.\n  * `Dynamic = true` can be set for the signal which makes the signal a dynamic signal meaning it will be called when\n    no other signals match. The call must accept a `string` for the signal name and `Temporalio.Converters.IRawValue[]`\n    for the arguments. Only one dynamic signal may be present on a workflow.\n* `[WorkflowQuery]` attribute may be present on any public method or property with public getter that handles queries.\n  * Query methods must be non-`void` but cannot return a `Task` (i.e. they cannot be async).\n  * The attribute can have a string argument for the query name. Otherwise the name is defaulted to the unqualified\n    method name.\n  * This attribute is not inherited and therefore must be explicitly set on any override.\n  * `Dynamic = true` can be set for the query which makes the query a dynamic query meaning it will be called when\n    no other queries match. The call must accept a `string` for the query name and `Temporalio.Converters.IRawValue[]`\n    for the arguments. Only one dynamic query may be present on a workflow.\n* `[WorkflowUpdate]` attribute may be present on any public method that handles updates.\n  * Update methods must return a `Task` (can be a `Task\u003cTResult\u003e`).\n  * The attribute can have a string argument for the update name. Otherwise the name is defaulted to the unqualified\n    method name with `Async` trimmed off the end if it is present.\n  * This attribute is not inherited and therefore must be explicitly set on any override.\n  * `Dynamic = true` can be set for the update which makes the update a dynamic update, meaning it will be called when\n    no other updates match. The call must accept a `string` for the update name and `Temporalio.Converters.IRawValue[]`\n    for the arguments. Only one dynamic update may be present on a workflow.\n  * A validator method can be created that is marked with the `[WorkflowUpdateValidator(nameof(MyUpdateMethod))]`\n    attribute. It must be `void` but accept the exact same parameters as the update method. This must be a read-only\n    method and if an exception is thrown, the update is failed without being stored in history.\n\n##### Workflow Inheritance\n\nWorkflows can inherit from interfaces and base classes. Callers can use these interfaces to make calls for a workflow\nwithout the implementation present. This can be valuable in separating logic, but there are some details that should be\nnoted.\n\n`[Workflow]` and `[WorkflowRun]` attributes are never inherited and must be defined on item that is actually registered\nwith the worker. This means even if an interface or base class has these, they must also be present on the final\nimplementing class. So if a base class has a full `[WorkflowRun]` implementation, the subclass must override that\nmethod, set `[WorkflowRun]` on the override, and then it can delegate to the base class. This explicit non-inheritance\nstrategy was intentionally done to avoid diamond problems with workflows and to let readers clearly know whether a class\nis a workflow (including the name defaulted) and what its entry point is. A workflow can only have one `[WorkflowRun]`\nmethod.\n\n`[WorkflowSignal]`, `[WorkflowQuery]`, and `[WorkflowUpdate]` methods can be inherited from base classes/interfaces if\nthe method is not overridden. However, if the method is declared in the subclass, it must also have these attributes.\nThe attributes themselves are not inherited.\n\n#### Running Workflows\n\nTo start a workflow from a client, you can `StartWorkflowAsync` with a lambda expression and then use the resulting\nhandle:\n\n```csharp\n// Start the workflow\nvar arg = new GreetingParams(Name: \"Temporal\");\nvar handle = await client.StartWorkflowAsync(\n    (GreetingWorkflow wf) =\u003e wf.RunAsync(arg),\n    new(id: \"my-workflow-id\", taskQueue: \"my-task-queue\"));\n// Check current greeting via query\nConsole.WriteLine(\n    \"Current greeting: {0}\",\n    await handle.QueryAsync(wf =\u003e wf.CurrentGreeting()));\n// Change the params via update\nvar updateArg = new GreetingParams(Salutation: \"Aloha\", Name: \"John\");\nawait handle.ExecuteUpdateAsync(wf =\u003e wf.UpdateGreetingParamsAsync(updateArg));\n// Tell it to complete via signal\nawait handle.SignalAsync(wf =\u003e wf.CompleteWithGreetingAsync());\n// Wait for workflow result\nConsole.WriteLine(\n    \"Final greeting: {0}\",\n    await handle.GetResultAsync());\n```\n\nSome things to note about the above code:\n\n* This uses the `GreetingWorkflow` from the previous section.\n* The output of this code is \"Current greeting: Hello, Temporal!\" and \"Final greeting: Aloha, John!\".\n* ID and task queue are required for starting a workflow.\n* All calls here are typed. For example, using something besides `GreetingParams` for the parameter of\n  `StartWorkflowAsync` would be a compile-time failure.\n* The `handle` is also typed with the workflow result, so `GetResultAsync()` returns a `string` as expected.\n* A shortcut extension `ExecuteWorkflowAsync` is available that is just `StartWorkflowAsync` + `GetResultAsync`.\n* `SignalWithStart` method is present on the workflow options to make the workflow call a signal-with-start call which\n  means it will only start the workflow if it's not running, but send a signal to it regardless.\n\n#### Invoking Activities\n\n* Activities are executed with `Workflow.ExecuteActivityAsync` which accepts a lambda expression that invokes the\n  activity with its arguments. The activity method can be sync or async, return a result or not, and be static or an\n  instance method (which would require the parameter of the lambda to be the instance type).\n* A non-type-safe form of `ExecuteActivityAsync` exists that just accepts a string activity name.\n* Activity options are a simple class set after the lambda expression or name.\n  * These options must be present and either `ScheduleToCloseTimeout` or `StartToCloseTimeout` must be present.\n  * Retry policy, cancellation type, etc can also be set on the options.\n  * Cancellation token is defaulted as the workflow cancellation token, but an alternative can be given in options.\n    When the token is cancelled, a cancellation request is sent to the activity. How that is handled depends on\n    cancellation type.\n* Activity failures are thrown from the task as `ActivityFailureException`.\n* `ExecuteLocalActivityAsync` exists with mostly the same options for local activities.\n\n#### Invoking Child Workflows\n\n* Child workflows are started with `Workflow.StartChildWorkflowAsync` which accepts a lambda expression whose parameter\n  is the child workflow to call and the expression is a call to its run method with arguments.\n* A non-type-safe form of `StartChildWorkflowAsync` exists that just accepts a string workflow name.\n* Child workflow options are a simple class set after after the lambda expression or name.\n  * These options are optional.\n  * Retry policy, ID, etc can also be set on the options.\n  * Cancellation token is defaulted as the workflow cancellation token, but an alternative can be given in options.\n    When the token is cancelled, a cancellation request is sent to the child workflow. How that is handled depends on\n    cancellation type.\n* Result of a child workflow starting is a `ChildWorkflowHandle` which has the `ID`, `GetResultAsync` for getting the\n  result, and `SignalAsync` for signalling the child.\n* The task for starting a child workflow does not complete until the start has been accepted by the server.\n* A shortcut of `Workflow.ExecuteChildWorkflowAsync` is available which is `StartChildWorkflowAsync` + `GetResultAsync`\n  for those only needing to wait on its result.\n\n#### Timers and Conditions\n\n* A timer is represented by `Workflow.DelayAsync`.\n  * Timers are also started on `Workflow.WaitConditionAsync` when a timeout is specified.\n  * This can accept a cancellation token, but if none given, defaults to `Workflow.CancellationToken`.\n  * `Task.Delay` or any other .NET timer-related call cannot be used in workflows because workflows must be\n    deterministic. See the \"Workflow Logic Constraints\" section below.\n* `Workflow.WaitConditionAsync` accepts a function that, when it returns true, the `Task` is completed successfully.\n  * The function is invoked on each iteration of the internal event loop. This is commonly used for checking if a\n    variable is changed from some other part of a workflow (e.g. a signal handler).\n  * A timeout can be provided for the wait condition which uses a timer.\n  * This can accept a cancellation token, but if none given, defaults to `Workflow.CancellationToken`.\n\n#### Workflow Task Scheduling and Cancellation\n\nWorkflows are backed by a custom, deterministic\n[TaskScheduler](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler?view=net-7.0). All\nasync calls inside a workflow must use this scheduler (i.e. `TaskScheduler.Current`) and not the default\nthread-pool-based one (i.e. `TaskScheduler.Default`). See \"Workflow Logic Constraints\" on what to avoid to make sure the\nproper task scheduler is used.\n\nEvery workflow contains a cancellation token at `Workflow.CancellationToken`. This token is cancelled when the workflow\nis cancelled. For all workflow calls that accept a cancellation token, this is the default. So if a workflow is waiting\non `ExecuteActivityAsync` and the workflow is cancelled, that cancellation will propagate to the waiting activity.\n\nCancellation token sources may be used to perform cancellation more specifically. A cancellation token derived from the\nworkflow one can be created via `CancellationTokenSource.CreateLinkedTokenSource(Workflow.CancellationToken)`. Then that\nsource can be used to cancel something more specifically. Or, in cases where cleanup code may need to be run during\ncancellation such as in a `finally` block, a new unlinked cancellation token source can be constructed that will not be\nseen as cancelled even though the workflow is cancelled.\n\nLike in other areas of .NET, cancellation tokens must be respected in order to properly cancel the workflow. Yet for\nmost use cases where await calls yield to Temporal, the default cancellation token at the workflow level is good enough.\n\n#### Workflow Utilities\n\nIn addition to the pieces documented above, additional properties/methods are statically available on `Workflow` that\ncan be used from workflows including:\n\n* Properties:\n  * `Info` - Immutable workflow info.\n  * `InWorkflow` - Boolean saying whether the current code is running in a workflow. This is the only call that won't\n    throw an exception when accessed outside of a workflow.\n  * `Logger` - Scoped replay-aware logger for use inside a workflow. Normal loggers should not be used because they may\n    log duplicate values during replay.\n  * `Memo` - Read-only current memo values.\n  * `PayloadConverter` - Can be used if `IRawValue` is used for input or output.\n  * `Queries` - Mutable set of query definitions for the workflow. Technically this can be mutated to add query\n    definitions at runtime, but methods with the `[WorkflowQuery]` attribute are strongly preferred.\n  * `Random` - Deterministically seeded random instance for use inside workflows.\n  * `TypedSearchAttributes` - Read-only current search attribute values.\n  * `UtcNow` - Deterministic value for the current `DateTime`.\n  * `Unsafe.IsReplaying` - For advanced users to know whether the workflow is replaying. This is rarely needed.\n* Methods:\n  * `CreateContinueAsNewException` - Create exception that can be thrown to perform a continue-as-new on the workflow.\n    There are several overloads to properly accept a lambda expression for the workflow similar to start/execute\n    workflow calls elsewhere.\n  * `GetExternalWorkflowHandle` - Get a handle to an external workflow to issue cancellation requests and signals.\n  * `NewGuid` - Create a deterministically random UUIDv4 GUID.\n  * `Patched` and `DeprecatePatch` - Support for patch-based versioning inside the workflow.\n  * `UpsertMemo` - Update the memo values for the workflow.\n  * `UpsertTypedSearchAttributes` - Update the search attributes for the workflow.\n\n#### Workflow Exceptions\n\n* Workflows can throw exceptions to fail the workflow/update or the \"workflow task\" (i.e. suspend the workflow, retrying\n  until code update allows it to continue).\n* By default, exceptions that are instances of `Temporalio.Exceptions.FailureException` will fail the workflow/update\n  with that exception.\n  * For failing the workflow/update explicitly with a user exception, explicitly throw\n    `Temporalio.Exceptions.ApplicationFailureException`. This can be marked non-retryable or include details as needed.\n  * Other exceptions that come from activity execution, child execution, cancellation, etc are already instances of\n    `FailureException` (or `TaskCanceledException`) and will fail the workflow/update if uncaught.\n* By default, all other exceptions fail the \"workflow task\" which means the workflow/update will continually retry until\n  the code is fixed. This is helpful for bad code or other non-predictable exceptions. To actually fail the\n  workflow/update, use an `ApplicationFailureException` as mentioned above.\n* By default, all non-deterministic exceptions that are detected internally fail the \"workflow task\".\n\nThe default behavior can be customized at the worker level for all workflows via the\n`TemporalWorkerOptions.WorkflowFailureExceptionTypes` property or per workflow via the `FailureExceptionTypes` property\non the `WorkflowAttribute`. When a workflow encounters a \"workflow task\" fail (i.e. suspend), it will first check either\nof these collections to see if the exception is an instance of any of the types and if so, will turn into a\nworkflow/update failure. As a special case, when a non-deterministic exception occurs and\n`Temporalio.Exceptions.WorkflowNondeterminismException` is assignable to any of the types in the collection, that too\nwill turn into a workflow/update failure. However non-deterministic exceptions that match during update handlers become\nworkflow failures not update failures like other exceptions because a non-deterministic exception is an\nentire-workflow-failure situation.\n\n⚠️ WARNING: Customizing the default behavior is currently experimental and the default behavior may change in the\nfuture.\n\n#### Workflow Logic Constraints\n\nTemporal Workflows [must be deterministic](https://docs.temporal.io/workflows#deterministic-constraints) which includes\n.NET workflows. This means there are several things workflows cannot do such as:\n\n* Perform IO (network, disk, stdio, etc)\n* Access/alter external mutable state\n* Do any threading\n* Do anything using the system clock (e.g. `DateTime.Now`)\n  * This includes  .NET timers (e.g. `Task.Delay` or `Thread.Sleep`)\n* Make any random calls\n* Make any not-guaranteed-deterministic calls (e.g. iterating over a dictionary)\n\nIn the future, an analyzer may be written to help catch some of these mistakes at compile time. In the meantime, due to\n.NET's lack of a sandbox, there is not a good way to prevent non-deterministic calls so developers need to be vigilant.\n\n##### .NET Task Determinism\n\nSome calls in .NET do unsuspecting non-deterministic things and are easy to accidentally use. This is especially true\nwith `Task`s. Temporal requires that the deterministic `TaskScheduler.Current` is used, but many .NET async calls will\nuse `TaskScheduler.Default` implicitly (and some analyzers even encourage this). Here are some known gotchas to avoid\nwith .NET tasks inside of workflows:\n\n* Do not use `Task.Run` - this uses the default scheduler and puts work on the thread pool.\n  * Use `Workflow.RunTaskAsync` instead.\n  * Can also use `Task.Factory.StartNew` with current scheduler or instantiate the `Task` and run `Task.Start` on it.\n* Do not use `Task.ConfigureAwait(false)` - this will not use the current context.\n  * If you must use `Task.ConfigureAwait`, use `Task.ConfigureAwait(true)`.\n  * There is no significant performance benefit to `Task.ConfigureAwait` in workflows anyways due to how the scheduler\n    works.\n* Do not use anything that defaults to the default task scheduler.\n* Do not use `Task.Delay`, `Task.Wait`, timeout-based `CancellationTokenSource`, or anything that uses .NET built-in\n  timers.\n  * `Workflow.DelayAsync`, `Workflow.WaitConditionAsync`, or non-timeout-based cancellation token source is suggested.\n* Do not use `Task.WhenAny`.\n  * Use `Workflow.WhenAnyAsync` instead.\n  * Technically this only applies to an enumerable set of tasks with results or more than 2 tasks with results. Other\n    uses are safe. See [this issue](https://github.com/dotnet/runtime/issues/87481).\n* Do not use `Task.WhenAll`\n  * Use `Workflow.WhenAllAsync` instead.\n  * Technically `Task.WhenAll` is currently deterministic in .NET and safe, but it is better to use the wrapper to be\n    sure.\n* Do not use `CancellationTokenSource.CancelAsync`.\n  * Use `CancellationTokenSource.Cancel` instead.\n* Do not use `System.Threading.Semaphore` or `System.Threading.SemaphoreSlim` or `System.Threading.Mutex`.\n  * Use `Temporalio.Workflows.Semaphore` or `Temporalio.Workflows.Mutex` instead.\n  * _Technically_ `SemaphoreSlim` does work if only the async form of `WaitAsync` is used without no timeouts and\n    `Release` is used. But anything else can deadlock the workflow and its use is cumbersome since it must be disposed.\n* Be wary of additional libraries' implicit use of the default scheduler.\n  * For example, while there are articles for `Dataflow` about\n    [using a specific scheduler](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-specify-a-task-scheduler-in-a-dataflow-block),\n    there are hidden implicit uses of `TaskScheduler.Default`. For example, see\n    [this bug](https://github.com/dotnet/runtime/issues/83159).\n\nIn order to help catch wrong scheduler use, by default the Temporal .NET SDK adds an event source listener for\ninfo-level task events. While this technically receives events from all uses of tasks in the process, we make sure to\nignore anything that is not running in a workflow in a high performant way (basically one thread local check). For code\nthat does run in a workflow and accidentally starts a task in another scheduler, an `InvalidWorkflowOperationException`\nwill be thrown which \"pauses\" the workflow (fails the workflow task which continually retries until the code is fixed.).\nThis is unfortunately a runtime-only check, but can help catch mistakes early. If this needs to be turned off for any\nreason, set `DisableWorkflowTracingEventListener` to `true` in worker options.\n\nIn the near future for modern .NET versions we hope to use the\n[new `TimeProvider` API](https://github.com/dotnet/runtime/issues/36617) which will allow us to control current time and\ntimers.\n\n##### Workflow .editorconfig\n\nSince workflow code follows some different logic rules than regular C# code, there are some common analyzer rules out\nthere that developers may want to disable. To ensure these are only disabled for workflows, current recommendation is to\nuse the `.workflow.cs` extension for files containing workflows.\n\nHere are the rules to disable:\n\n* [CA1024](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1024) - This encourages\n  properties instead of methods that look like getters. However for reflection reasons we cannot use property getters\n  for queries, so it is very normal to have\n\n    ```csharp\n    [WorkflowQuery]\n    public string GetSomeThing() =\u003e someThing;\n    ```\n\n* [CA1822](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1822) - This encourages\n  static methods when methods don't access instance state. Workflows however use instance methods for run, signals,\n  queries, or updates even if they could be static.\n* [CA2007](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2007) - This encourages\n  users to use `ConfigureAwait` instead of directly waiting on a task. But in workflows, there is no benefit to this and\n  it just adds noise (and if used, needs to be `ConfigureAwait(true)` not `ConfigureAwait(false)`).\n* [CA2008](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2008) - This encourages\n  users to always apply an explicit task scheduler because the default of `TaskScheduler.Current` is bad. But for\n  workflows, the default of `TaskScheduler.Current` is good/required.\n* [CA5394](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca5394) - This discourages\n  use of non-crypto random. But deterministic workflows, via `Workflow.Random` intentionally provide a deterministic\n  non-crypto random instance.\n* `CS1998` - This discourages use of `async` on async methods that don't `await`. But workflows handlers like signals\n  are often easier to write in one-line form this way, e.g.\n  `public async Task SignalSomethingAsync(string value) =\u003e this.value = value;`.\n* [VSTHRD105](https://github.com/microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD105.md) -  This is similar to\n  `CA2008` above in that use of implicit current scheduler is discouraged. That does not apply to workflows where it is\n  encouraged/required.\n\nHere is the `.editorconfig` snippet for the above which may frequently change as we learn more:\n\n```ini\n##### Configuration specific for Temporal workflows #####\n[*.workflow.cs]\n\n# We use getters for queries, they cannot be properties\ndotnet_diagnostic.CA1024.severity = none\n\n# Don't force workflows to have static methods\ndotnet_diagnostic.CA1822.severity = none\n\n# Do not need ConfigureAwait for workflows\ndotnet_diagnostic.CA2007.severity = none\n\n# Do not need task scheduler for workflows\ndotnet_diagnostic.CA2008.severity = none\n\n# Workflow randomness is intentionally deterministic\ndotnet_diagnostic.CA5394.severity = none\n\n# Allow async methods to not have await in them\ndotnet_diagnostic.CS1998.severity = none\n\n# Don't avoid, but rather encourage things using TaskScheduler.Current in workflows\ndotnet_diagnostic.VSTHRD105.severity = none\n```\n\n#### Workflow Testing\n\nWorkflow testing can be done in an integration-test fashion against a real server, however it is hard to simulate\ntimeouts and other long time-based code. Using the time-skipping workflow test environment can help there.\n\nA non-time-skipping `Temporalio.Testing.WorkflowEnvironment` can be started via `StartLocalAsync` which supports all\nstandard Temporal features. It is actually a real Temporal server lazily downloaded on first use and run as a\nsub-process in the background.\n\nA time-skipping `Temporalio.Testing.WorkflowEnvironment` can be started via `StartTimeSkippingAsync` which is a\nreimplementation of the Temporal server with special time skipping capabilities. This too lazily downloads the process\nto run when first called. Note, this class is not thread safe nor safe for use with independent tests. It can be reused,\nbut only for one test at a time because time skipping is locked/unlocked at the environment level.\n\n##### Automatic Time Skipping\n\nAnytime a workflow result is waiting on, the time-skipping server automatically advances to the next event it can. To\nmanually advance time before waiting on the result of the workflow, the `WorkflowEnvironment.DelayAsync` method can be\nused. If an activity is running, time-skipping is disabled.\n\nHere's a simple example of a workflow that sleeps for 24 hours:\n\n```csharp\nusing Temporalio.Workflows;\n\n[Workflow]\npublic class WaitADayWorkflow\n{\n    [WorkflowRun]\n    public async Task\u003cstring\u003e RunAsync()\n    {\n        await Workflow.DelayAsync(TimeSpan.FromDays(1));\n        return \"all done\";\n    }\n}\n```\n\nA regular integration test of this workflow on a normal server would be way too slow. However, the time-skipping server\nautomatically skips to the next event when we wait on the result. Here's a test for that workflow:\n\n```csharp\nusing Temporalio.Testing;\nusing Temporalio.Worker;\n\n[Fact]\npublic async Task WaitADayWorkflow_SimpleRun_Succeeds()\n{\n    await using var env = await WorkflowEnvironment.StartTimeSkippingAsync();\n    using var worker = new TemporalWorker(\n      env.Client,\n      new TemporalWorkerOptions($\"task-queue-{Guid.NewGuid()}\").\n          AddWorkflow\u003cWaitADayWorkflow\u003e());\n    await worker.ExecuteAsync(async () =\u003e\n    {\n        var result = await env.Client.ExecuteWorkflowAsync(\n            (WaitADayWorkflow wf) =\u003e wf.RunAsync(),\n            new(id: $\"wf-{Guid.NewGuid()}\", taskQueue: worker.Options.TaskQueue!));\n        Assert.Equal(\"all done\", result);\n    });\n}\n```\n\nThis test will run almost instantly. This is because by calling `ExecuteWorkflowAsync` on our client, we are actually\ncalling `StartWorkflowAsync` + `GetResultAsync`, and `GetResultAsync` automatically skips time as much as it can\n(basically until the end of the workflow or until an activity is run).\n\nTo disable automatic time-skipping while waiting for a workflow result, run code as a lambda passed to\n`env.WithAutoTimeSkippingDisabled` or `env.WithAutoTimeSkippingDisabledAsync`.\n\n##### Manual Time Skipping\n\nUntil a workflow is waited on, all time skipping in the time-skipping environment is done manually via\n`WorkflowEnvironment.DelayAsync`.\n\nHere's a workflow that waits for a signal or times out:\n\n```csharp\nusing Temporalio.Workflows;\n\n[Workflow]\npublic class SignalWorkflow\n{\n    private bool signalReceived = false;\n\n    [WorkflowRun]\n    public async Task\u003cstring\u003e RunAsync()\n    {\n        // Wait for signal or timeout in 45 seconds\n        if (Workflow.WaitConditionAsync(() =\u003e signalReceived, TimeSpan.FromSeconds(45)))\n        {\n            return \"got signal\";\n        }\n        return \"got timeout\";\n    }\n\n    [WorkflowSignal]\n    public async Task SomeSignalAsync() =\u003e signalReceived = true;\n}\n```\n\nTo test a normal signal, you might:\n\n```csharp\nusing Temporalio.Testing;\nusing Temporalio.Worker;\n\n[Fact]\npublic async Task SignalWorkflow_SendSignal_HasExpectedResult()\n{\n    await using var env = WorkflowEnvironment.StartTimeSkippingAsync();\n    using var worker = new TemporalWorker(\n        env.Client,\n        new TemporalWorkerOptions($\"task-queue-{Guid.NewGuid()}\").\n            AddWorkflow\u003cSignalWorkflow\u003e());\n    await worker.ExecuteAsync(async () =\u003e\n    {\n        var handle = await env.Client.StartWorkflowAsync(\n            (SignalWorkflow wf) =\u003e wf.RunAsync(),\n            new(id: $\"wf-{Guid.NewGuid()}\", taskQueue: worker.Options.TaskQueue!));\n        await handle.SignalAsync(wf =\u003e wf.SomeSignalAsync());\n        Assert.Equal(\"got signal\", await handle.GetResultAsync());\n    });\n}\n```\n\nBut how would you test the timeout part? Like so:\n\n```csharp\nusing Temporalio.Testing;\nusing Temporalio.Worker;\n\n[Fact]\npublic async Task SignalWorkflow_SignalTimeout_HasExpectedResult()\n{\n    await using var env = WorkflowEnvironment.StartTimeSkippingAsync();\n    using var worker = new TemporalWorker(\n        env.Client,\n        new TemporalWorkerOptions($\"task-queue-{Guid.NewGuid()}\").\n            AddWorkflow\u003cSignalWorkflow\u003e());\n    await worker.ExecuteAsync(async () =\u003e\n    {\n        var handle = await env.Client.StartWorkflowAsync(\n            (SignalWorkflow wf) =\u003e wf.RunAsync(),\n            new(id: $\"wf-{Guid.NewGuid()}\", taskQueue: worker.Options.TaskQueue!));\n        await env.DelayAsync(TimeSpan.FromSeconds(50));\n        Assert.Equal(\"got timeout\", await handle.GetResultAsync());\n    });\n}\n```\n\n##### Mocking Activities\n\nWhen testing workflows, often you don't want to actually run the activities. Activities are just functions with the\n`[Activity]` attribute. Simply write different/empty/fake/asserting ones and pass those to the worker to have different\nactivities called during the test.\n\n#### Workflow Replay\n\nGiven a workflow's history, it can be replayed locally to check for things like non-determinism errors. For example,\nassuming the `history` parameter below is given a JSON string of history exported from the CLI or web UI, the following\nfunction will replay it:\n\n```csharp\nusing Temporalio;\nusing Temporalio.Worker;\n\npublic static async Task ReplayFromJsonAsync(string historyJson)\n{\n    var replayer = new WorkflowReplayer(\n      new WorkflowReplayerOptions().AddWorkflow\u003cMyWorkflow\u003e());\n    await replayer.ReplayWorkflowAsync(WorkflowHistory.FromJson(\"my-workflow-id\", historyJson));\n}\n```\n\nIf there is a non-determinism, this will throw an exception.\n\nWorkflow history can be loaded from more than just JSON. It can be fetched individually from a workflow handle, or even\nin a list. For example, the following code will check that all workflow histories for a certain workflow type (i.e.\nworkflow class) are safe with the current workflow code.\n\n```csharp\nusing Temporalio;\nusing Temporalio.Client;\nusing Temporalio.Worker;\n\npublic static async Task CheckPastHistoriesAysnc(ITemporalClient client)\n{\n    var replayer = new WorkflowReplayer(\n      new WorkflowReplayerOptions().AddWorkflow\u003cMyWorkflow\u003e());\n    var listIter = client.ListWorkflowHistoriesAsync(\"WorkflowType = 'SayHello'\");\n    await foreach (var result in replayer.ReplayWorkflowsAsync(listIter))\n    {\n        if (result.ReplayFailure != null)\n        {\n            ExceptionDispatchInfo.Throw(result.ReplayFailure);\n        }\n    }\n}\n```\n\n### Activities\n\n#### Activity Definition\n\nActivities are methods with the `[Activity]` annotation like so:\n\n```csharp\nnamespace MyNamespace;\n\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing System.Timers;\nusing Temporalio.Activities;\n\npublic static class MyActivities\n{\n    private static readonly HttpClient client = new();\n\n    [Activity]\n    public static async Task\u003cstring\u003e GetPageAsync(string url)\n    {\n        // Heartbeat every 2s\n        using var timer = new Timer(2000)\n        {\n            AutoReset = true,\n            Enabled = true,\n        };\n        timer.Elapsed += (sender, eventArgs) =\u003e ActivityExecutionContext.Current.Heartbeat();\n\n        // Issue our HTTP call\n        using var response = await client.GetAsync(url, ActivityExecutionContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n        return await response.Content.ReadAsStringAsync(ActivityExecutionContext.Current.CancellationToken);\n    }\n}\n```\n\nNotes about activity definitions:\n\n* All activities must have the `[Activity]` attribute.\n* `[Activity]` can be given a custom string name.\n  * If unset, the default is the method's unqualified name. If the method name ends with `Async` and returns a `Task`,\n    the default name will have `Async` trimmed off the end.\n* Long running activities should heartbeat to regularly to inform server the activity is still running.\n  * Heartbeats are throttled internally, so users can call this frequently without fear of calling too much.\n  * Activities must heartbeat to receive cancellation.\n* Activities can be defined on static or instance methods. They can even be lambdas or local methods, but rarely is this\n  valuable since often an activity will be referenced by a workflow.\n* Activities can be synchronous or asynchronous. If an activity returns a `Task`, that task is awaited on as part of the\n  activity.\n* `[Activity(Dynamic = true)` represents a dynamic activity meaning it will be called when no other activities match.\n  The call must accept a single parameter of `Temporalio.Converters.IRawValue[]` for the arguments. Only one dynamic\n  activity may be registered on a worker.\n\n#### Activity Dependency Injection\n\nTo have activity classes instantiated via a DI container to support dependency injection, see the\n[Temporalio.Extensions.Hosting](https://github.com/temporalio/sdk-dotnet/tree/main/src/Temporalio.Extensions.Hosting/)\nproject which supports worker services in addition to activity dependency injection.\n\n#### Activity Execution Context\n\nDuring activity execution, an async-local activity context is available via `ActivityExecutionContext.Current`. This\nwill throw if not currently in an activity context (which can be checked with `ActivityExecutionContext.HasCurrent`). It\ncontains the following important members:\n\n* `Info` - Information about the activity.\n* `Logger` - A logger scoped to the activity.\n* `CancelReason` - If `CancellationToken` is cancelled, this will contain the reason.\n* `CancellationToken` - Token cancelled when the activity is cancelled.\n* `Heartbeat(object?...)` - Send a heartbeat from this activity.\n* `WorkerShutdownToken` - Token cancelled on worker shutdown before the grace period + `CancellationToken` cancellation.\n* `PayloadConverter` - Can be used if `IRawValue` is used for input or output.\n\n#### Activity Heartbeating and Cancellation\n\nIn order for a non-local activity to be notified of cancellation requests, it must invoke\n`ActivityExecutionContext.Current.Heartbeat()`. It is strongly recommended that all but the fastest executing activities\ncall this function regularly.\n\nIn addition to obtaining cancellation information, heartbeats also support detail data that is persisted on the server\nfor retrieval during activity retry. If an activity calls `ActivityExecutionContext.Current.Heartbeat(123)` and then\nfails and is retried, `ActivityExecutionContext.Current.Info.HeartbeatDetails` will contain the last detail payloads. A\nhelper can be used to convert, so `await ActivityExecutionContext.Current.Info.HeartbeatDetailAtAsync\u003cint\u003e(0)` would\ngive `123` on the next attempt.\n\nHeartbeating has no effect on local activities.\n\n#### Activity Worker Shutdown\n\nAn activity can react to a worker shutdown specifically.\n\nUpon worker shutdown, `ActivityExecutionContext.WorkerShutdownToken` is cancelled. Then the worker will wait a grace\nperiod set by the `GracefulShutdownTimeout` worker option (default as 0) before issuing actual cancellation to all\nstill-running activities via `ActivityExecutionContext.CancellationToken`.\n\nWorker shutdown will wait on all activities to complete, so if a long-running activity does not respect cancellation,\nthe shutdown may never complete.\n\n#### Activity Testing\n\nUnit testing an activity or any code that could run in an activity is done via the\n`Temporalio.Testing.ActivityEnvironment` class. Simply instantiate the class, and any function passed to `RunAsync` will\nbe invoked inside the activity context. The following important members are available on the environment to affect the\nactivity context:\n\n* `Info` - Activity info, defaulted to a basic set of values.\n* `Logger` - Activity logger, defaulted to a null logger.\n* `Cancel(CancelReason)` - Helper to set the reason and cancel the source.\n* `CancelReason` - Cancel reason.\n* `CancellationTokenSource` - Token source for issuing cancellation.\n* `Heartbeater` - Callback invoked each heartbeat.\n* `WorkerShutdownTokenSource` - Token source for issuing worker shutdown.\n* `PayloadConverter` - Defaulted to default payload converter.\n\n### OpenTelemetry Tracing Support\n\nSee the\n[OpenTelemetry extension](https://github.com/temporalio/sdk-dotnet/tree/main/src/Temporalio.Extensions.OpenTelemetry/).\n\n### Built-in Native Shared Library\n\nThis SDK requires a built-in unmanaged, native shared library built in Rust. It is named `temporal_sdk_bridge.dll` on\nWindows, `libtemporal_sdk_bridge.so` on Linux, and `libtemporal_sdk_bridge.dylib` on macOS. This is automatically\nincluded when using modern versions of .NET on a common platform. If you are using .NET framework, you may have to\nexplicitly set the platform to `x64` or `arm64` because `AnyCPU` will not choose the proper library.\n\nCurrently we only support [RIDs](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog) `linux-arm64`,\n`linux-x64`, `osx-arm64`, `osx-x64`, and `win-x64`. Any other platforms needed (e.g. `linux-musl-x64` on Alpine) will\nrequire a custom build.\n\nThe native shared library on Windows does require a Visual C++ runtime. Some containers, such as Windows Nano Server, do\nnot include this runtime. If not available, users may have to manually copy this runtime (usually just\n`vcruntime140.dll`), depend on a NuGet package that has it, or install the Visual C++ runtime (often via Visual C++\nRedistributable installation).\n\nIf the native shared library is not loading for whatever reason, the following error may appear:\n\n\u003e System.DllNotFoundException: Unable to load DLL 'temporal_sdk_bridge' or one of its dependencies: The specified module\ncould not be found.\n\nSee the earlier part of this section for details on what environments are supported.\n\n## Development\n\n### Build\n\nPrerequisites:\n\n* [.NET](https://learn.microsoft.com/en-us/dotnet/core/install/)\n* [Rust](https://www.rust-lang.org/) (i.e. `cargo` on the `PATH`)\n* [Protobuf Compiler](https://protobuf.dev/) (i.e. `protoc` on the `PATH`)\n* This repository, cloned recursively\n\nWith all prerequisites in place, run:\n\n    dotnet build\n\nOr for release:\n\n    dotnet build --configuration Release\n\n### Code formatting\n\nThis project uses StyleCop analyzers with some overrides in `.editorconfig`. To format, run:\n\n    dotnet format\n\nCan also run with `--verify-no-changes` to ensure it is formatted.\n\n#### VisualStudio Code\n\nWhen developing in vscode, the following JSON settings will enable StyleCop analyzers:\n\n```json\n    \"omnisharp.enableEditorConfigSupport\": true,\n    \"omnisharp.enableRoslynAnalyzers\": true\n```\n\n### Testing\n\nRun:\n\n    dotnet test\n\nCan add options like:\n\n* `--logger \"console;verbosity=detailed\"` to show logs\n* `--filter \"FullyQualifiedName=Temporalio.Tests.Client.TemporalClientTests.ConnectAsync_Connection_Succeeds\"` to run a\n  specific test\n* `--blame-crash` to do a host process dump on crash\n\nTo help debug native pieces and show full stdout/stderr, this is also available as an in-proc test program. Run:\n\n    dotnet run --project tests/Temporalio.Tests\n\nExtra args can be added after `--`, e.g. `-- -verbose` would show verbose logs and `-- --help` would show other\noptions. If the arguments are anything but `--help`, the current assembly is prepended to the args before sending to the\nxUnit runner.\n\nThe following environment variables can be set to override the environment:\n\n* `TEMPORAL_TEST_CLIENT_TARGET_HOST` - This must be set for any of the variables below to apply\n* `TEMPORAL_TEST_CLIENT_NAMESPACE` - Required if the above is set\n* `TEMPORAL_TEST_CLIENT_CERT` - Optional, must be present if below is\n* `TEMPORAL_TEST_CLIENT_KEY` - Optional, must be present if above is\n\n### Rebuilding Rust extension and interop layer\n\nTo regen core interop from header, install\n[ClangSharpPInvokeGenerator](https://github.com/dotnet/ClangSharp#generating-bindings) like:\n\n    dotnet tool install --global ClangSharpPInvokeGenerator\n\nThen, run:\n\n    ClangSharpPInvokeGenerator @src/Temporalio/Bridge/GenerateInterop.rsp\n\nThe Rust DLL is built automatically when the project is built. `protoc` may need to be on the `PATH` to build the Rust\nDLL.\n\nThis can be annoying to install on linux - so alternatively, publish your PR and you can download the\npatch from the windows build when it fails because of a mismatch. It uploads the patch as an artifact.\n\n### Regenerating protos\n\nMust have `protoc` on the `PATH`. Note, for now users should use `protoc` 23.x until\n[our GH action downloader](https://github.com/arduino/setup-protoc/issues/99) can support later versions.\n[Here](https://github.com/protocolbuffers/protobuf/releases/tag/v23.4) is the latest 23.x release as of this writing.\n\nThen:\n\n    dotnet run --project src/Temporalio.Api.Generator\n\n### Regenerating API docs\n\nInstall [docfx](https://dotnet.github.io/docfx/), then run:\n\n    docfx src/Temporalio.ApiDoc/docfx.json\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftemporalio%2Fsdk-dotnet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftemporalio%2Fsdk-dotnet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftemporalio%2Fsdk-dotnet/lists"}