Skip to main content
Temporal .NET SDK

Run your first Temporal application with the .NET SDK

~15 minutes totalTemporal beginnerHands-on tutorial
  1. Understand the application
  2. Run the application
  3. Simulate failures

In this tutorial, you'll run your first Temporal Application using the .NET SDK. You'll use the Web UI for state visibility, then explore how Temporal helps you recover from common failures.

What you'll do
  • Explore Temporal's core terminology and concepts.
  • Run a Temporal Workflow Application using a Temporal Cluster and the .NET SDK.
  • Practice reviewing the state of the Workflow.
  • Understand the inherent reliability of Workflow methods.

Prerequisites

Application overview

The project simulates a money transfer application: withdrawals, deposits, and refunds. If the deposit fails after a successful withdrawal, the money returns to the original account via a compensating RefundAsync Activity.

Temporal automatically preserves application state when something fails - recovering processes where they left off or rolling them back.

Download the example application

The source code is available in a GitHub repository. Clone it:

git clone https://github.com/temporalio/money-transfer-project-template-dotnet
cd money-transfer-temporal-template-dotnet

Workflow Definition

A Workflow Definition in .NET is marked by the [Workflow] attribute on the class. The [WorkflowRun] attribute marks the entry-point method:

MoneyTransferWorker/Workflow.cs
namespace Temporalio.MoneyTransferProject.MoneyTransferWorker;
using Temporalio.MoneyTransferProject.BankingService.Exceptions;
using Temporalio.Workflows;
using Temporalio.Common;
using Temporalio.Exceptions;

[Workflow]
public class MoneyTransferWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(PaymentDetails details)
{
var retryPolicy = new RetryPolicy
{
InitialInterval = TimeSpan.FromSeconds(1),
MaximumInterval = TimeSpan.FromSeconds(100),
BackoffCoefficient = 2,
MaximumAttempts = 3,
NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" }
};

string withdrawResult;
try
{
withdrawResult = await Workflow.ExecuteActivityAsync(
() => BankingActivities.WithdrawAsync(details),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy }
);
}
catch (ApplicationFailureException ex) when (ex.ErrorType == "InsufficientFundsException")
{
throw new ApplicationFailureException("Withdrawal failed due to insufficient funds.", ex);
}

string depositResult;
try
{
depositResult = await Workflow.ExecuteActivityAsync(
() => BankingActivities.DepositAsync(details),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy }
);
return $"Transfer complete (transaction IDs: {withdrawResult}, {depositResult})";
}
catch (Exception depositEx)
{
try
{
string refundResult = await Workflow.ExecuteActivityAsync(
() => BankingActivities.RefundAsync(details),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy }
);
throw new ApplicationFailureException(
$"Failed to deposit money into account {details.TargetAccount}. Money returned to {details.SourceAccount}.", depositEx);
}
catch (Exception refundEx)
{
throw new ApplicationFailureException(
$"Failed to deposit into {details.TargetAccount}. Refund failed: {refundEx.Message}", refundEx);
}
}
}
}

The input type is a PaymentDetails record:

MoneyTransferWorker/PaymentDetails.cs
namespace Temporalio.MoneyTransferProject.MoneyTransferWorker;

public record PaymentDetails(
string SourceAccount,
string TargetAccount,
int Amount,
string ReferenceId);
tip

It's a good practice to send a single object into a Workflow as its input, rather than multiple separate input variables.

Activity Definition

Mark a method within a class as an Activity by adding the [Activity] attribute. The WithdrawAsync() Activity calls a service to process the withdrawal:

MoneyTransferWorker/Activities.cs
namespace Temporalio.MoneyTransferProject.MoneyTransferWorker;
using Temporalio.Activities;
using Temporalio.Exceptions;

public class BankingActivities
{
[Activity]
public static async Task<string> WithdrawAsync(PaymentDetails details)
{
var bankService = new BankingService("bank1.example.com");
Console.WriteLine($"Withdrawing ${details.Amount} from account {details.SourceAccount}.");
try
{
return await bankService.WithdrawAsync(details.SourceAccount, details.Amount, details.ReferenceId);
}
catch (Exception ex)
{
throw new ApplicationFailureException("Withdrawal failed", ex);
}
}
}

The DepositAsync() Activity looks almost identical:

MoneyTransferWorker/Activities.cs
[Activity]
public static async Task<string> DepositAsync(PaymentDetails details)
{
var bankService = new BankingService("bank2.example.com");
Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}.");

// Uncomment below and comment out the try-catch block below to simulate unknown failure
/*
return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId);
*/

try
{
return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId);
}
catch (Exception ex)
{
throw new ApplicationFailureException("Deposit failed", ex);
}
}

The commented block is what you'll uncomment later to simulate an unknown failure.

Why you use Activities

Temporal Workflows have deterministic constraints and must produce the same output each time, given the same input. Non-deterministic work like file or network access must be done by Activities.

Set the Retry Policy

If an Activity fails, Temporal Workflows automatically retry. The Retry Policy at the top of the Workflow customizes how:

MoneyTransferWorker/Workflow.cs
var retryPolicy = new RetryPolicy
{
InitialInterval = TimeSpan.FromSeconds(1),
MaximumInterval = TimeSpan.FromSeconds(100),
BackoffCoefficient = 2,
MaximumAttempts = 3,
NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" }
};

In this example, Temporal retries the failed Activity up to 3 times, with exponential backoff. If the Workflow encounters InsufficientFundsException or InvalidAccountException, it won't retry - and if the deposit fails, the Workflow attempts to refund the money to the source account.

This is a simplified example

In production you'd add more advanced logic - including a "human in the loop" step where someone is notified of refund issues and can intervene.

Get notified when we launch new educational content

New courses, tutorials, and learning resources - straight to your inbox.

Subscribe
Feedback