# Tutorial: Build a Dapr Workflow application with Aspire

In this tutorial you'll:

- Create a Dapr Workflow application with Aspire from scratch
- Run the application locally with the Dapr workflow engine
- Use the Dapr Dev Dashboard to monitor local Dapr workflow executions
- Run the application locally and use managed workflow engine provided by Catalyst
- Use Catalyst to monitor the workflows in the Catalyst web console

:::info
If you don't want to build an Aspire solution from scratch, you can clone the [**catalyst-samples**](https://github.com/diagrid-labs/catalyst-samples) repo, navigate to the `workflow/csharp/CatalystAspire` folder, and continue from step 6 or 7.
:::

## 1. Prerequisites

- A container runtime such as [Docker Desktop](https://www.docker.com/products/docker-desktop) or [Podman](https://podman.io/)
- [.NET 10](https://dotnet.microsoft.com/download)
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
- [Aspire CLI](https://aspire.dev/get-started/install-cli/)
- Required from step 7:
  - [A Diagrid Catalyst account](https://www.diagrid.io/catalyst)
  - [Diagrid CLI](https://docs.diagrid.io/references/catalyst/catalyst-cli-intro)

## 2. Scaffold the project

### 2.1 Create the Aspire project

Start by creating the Aspire application. Use the Aspire CLI to create an Aspire starter app:

```shell
aspire new aspire-starter -n CatalystAspireApp -o CatalystAspireApp
```

> Choose whether or not you want a test project added. Tests won't be used in this tutorial.

You should end up with a newly scaffolded Aspire starter solution in the `CatalystAspireApp` folder.

The solution contains:

- An AppHost project
- A ServiceDefaults project
- A Web project
- An ApiService project

### 2.2 Add dependencies

1. Next, add the following packages to enable the usage of Dapr Workflow:

   ```shell
   cd CatalystAspireApp
   dotnet add CatalystAspireApp.AppHost/CatalystAspireApp.AppHost.csproj package CommunityToolkit.Aspire.Hosting.Dapr
   dotnet add CatalystAspireApp.AppHost/CatalystAspireApp.AppHost.csproj package Aspire.Hosting.Valkey
   dotnet add CatalystAspireApp.ApiService/CatalystAspireApp.ApiService.csproj package Dapr.Workflow
   ```

2. Create a `Resources` folder in the `CatalystAspireApp.AppHost` folder.

   ```shell
   mkdir CatalystAspireApp.AppHost/Resources
   ```

3. Add a Dapr state store component file called `statestore.yaml` in the Resources folder, add the following content:

   ```yaml
   apiVersion: dapr.io/v1alpha1
   kind: Component
   metadata:
     name: workflow-store
   spec:
     type: state.redis
     version: v1
     metadata:
       - name: redisHost
         value: "localhost:16379"
       - name: redisPassword
         value: "zxczxc123"
       - name: actorStateStore
         value: "true"
   ```

4. Add another Dapr state store component file for the Dapr Dev Dashboard, called `statestore-dashboard.yaml`, in the Resources folder with the following content:

   ```yaml
   apiVersion: dapr.io/v1alpha1
   kind: Component
   metadata:
     name: workflow-store
   scopes:
     - diagrid-dashboard
   spec:
     type: state.redis
     version: v1
     metadata:
       - name: redisHost
         value: "host.docker.internal:16379"
       - name: redisPassword
         value: "zxczxc123"
       - name: actorStateStore
         value: "true"
   ```

5. Now update the `CatalystAspireApp.AppHost/CatalystAspireApp.AppHost.csproj` file to include the content of the `Resources` folder. This folder contains some Dapr state component files that are required for running stateful workflows and also to use the Dapr Dev Dashboard.

   ```xml
   <ItemGroup>
       <Content Include="Resources\**\*.*">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
           <!-- Optional: show files in Solution Explorer under a virtual folder -->
           <Link>Resources\%(RecursiveDir)%(Filename)%(Extension)</Link>
       </Content>
   </ItemGroup>
   ```

### 2.3 Configure the AppHost project

Continue by setting up the AppHost project to run your project with Dapr.

Using your favorite editor, open up the `CatalystAspireApp.AppHost/AppHost.cs` file and replace the entire content with the following:

```csharp
using System.Reflection;
using CommunityToolkit.Aspire.Hosting.Dapr;
using Microsoft.AspNetCore.Components.Authorization;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddDapr();

string executingPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new("Where am I?");

// This is the state store Dapr will use for the workflow state.
var cachePassword = builder.AddParameter("cache-password", "zxczxc123", secret: true);
var cache = builder
    .AddValkey("cache", 16379, cachePassword)
    .WithContainerName("workflow-state")
    .WithDataVolume("workflow-state-data")
    ;

// The ApiService will contain the Dapr Workflow definitions and requires a Dapr sidecar.
var apiService = builder.AddProject<Projects.CatalystAspireApp_ApiService>("apiservice")
    .WithDaprSidecar(new DaprSidecarOptions
        {
            LogLevel = "debug",
            ResourcesPaths =
            [
                Path.Join(executingPath, "Resources"),
            ],
        }
    );

apiService.WaitFor(cache);

// Configure the Dapr Dev Dashboard, you can access this via the Aspire Dashboard to inspect Dapr workflows.
builder
    .AddContainer("diagrid-dashboard", "ghcr.io/diagridio/diagrid-dashboard:latest")
    .WithContainerName("diagrid-dashboard")
    .WithBindMount(Path.Join(executingPath, "Resources"), "/app/components")
    .WithEnvironment("COMPONENT_FILE", "/app/components/statestore-dashboard.yaml")
    .WithEnvironment("APP_ID", "diagrid-dashboard")
    .WithHttpEndpoint(targetPort: 8080)
    .WithReference(cache)
    ;

// We're not using the web front-end project in this tutorial, but it can stay.
builder.AddProject<Projects.CatalystAspireApp_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithHttpHealthCheck("/health")
    .WithReference(apiService)
    .WaitFor(apiService);

builder.Build().Run();
```

## 3. Create the workflow

In the `ApiService` project, create a new class called `FirstWorkflow` with the following content:

```csharp
using Dapr.Workflow;

namespace CatalystAspireApp.ApiService;

public class FirstWorkflow : Workflow<FirstWorkflow.Input, FirstWorkflow.Output>
{
    public record Input
    {
        public required int Left { get; init; }
        public required int Right { get; init; }
    }

    public record Output
    {
        public required int Value { get; init; }
    }

    public override async Task<Output> RunAsync(WorkflowContext context, Input input)
    {
        var firstActivityResult = await context.CallActivityAsync<FirstActivity.Output>(
            nameof(FirstActivity), 
            new FirstActivity.Input
            {
                Left = input.Left,
                Right = input.Right,
            });

        await context.CreateTimer(TimeSpan.FromSeconds(3));
        
        var secondActivityResult = await context.CallActivityAsync<SecondActivity.Output>(
            nameof(SecondActivity),
            new SecondActivity.Input
            {
                Value = firstActivityResult.Sum,
            });
        
        return new()
        {
            Value = secondActivityResult.Value,
        };
    }
}
```

## 4. Create the activities

Create two sample activities to demonstrate how workflows complete work.

First, create a class called `FirstActivity` with the following content:

```csharp
using Dapr.Workflow;

namespace CatalystAspireApp.ApiService;

public class FirstActivity : WorkflowActivity<FirstActivity.Input, FirstActivity.Output>
{
    public record Input
    {
        public required int Left { get; init; }
        public required int Right { get; init; }
    }

    public record Output
    {
        public required int Sum { get; init; }
    }

    public override async Task<Output> RunAsync(WorkflowActivityContext context, Input input)
    {
        return new()
        {
            Sum = input.Left + input.Right,
        };
    }
}
```

Next create a second class called `SecondActivity` and use the following code:

```csharp
using Dapr.Workflow;

namespace CatalystAspireApp.ApiService;

public class SecondActivity : WorkflowActivity<SecondActivity.Input, SecondActivity.Output>
{
    public record Input
    {
        public required int Value { get; init; }
    }

    public record Output
    {
        public required int Value { get; init; }
    }

    public override async Task<Output> RunAsync(WorkflowActivityContext context, Input input)
    {
        return new()
        {
            Value = input.Value * 1000,
        };
    }
}
```

## 5. Add workflow management operations

You need a way to schedule new workflow executions and check their status. Add two endpoints to `CatalystAspireApp.ApiService/Program.cs`.

1. Add the following `using`s to the `CatalystAspireApp.ApiService/Program.cs`:

   ```csharp
   using Microsoft.AspNetCore.Mvc;
   using CatalystAspireApp.ApiService;
   using Dapr.Workflow;
   ```

2. Register the workflow and the two activities with the `AddDaprWorkflow` extension method:

   ```csharp
   builder.Services.AddDaprWorkflow(options =>
   {
       options.RegisterWorkflow<FirstWorkflow>();
       options.RegisterActivity<FirstActivity>();
       options.RegisterActivity<SecondActivity>();
   });
   ```

3. Add a `start` POST endpoint that schedules a new Dapr workflow execution:

   ```csharp
   app.MapPost("/start", async ([FromServices] DaprWorkflowClient workflowClient) =>
   {
       var instanceId = await workflowClient.ScheduleNewWorkflowAsync(
           nameof(FirstWorkflow),
           input: new FirstWorkflow.Input
           {
               Left = 5,
               Right = 10,
           });
       return Results.Ok(new { instanceId });
   });
   ```

4. Add a `status`GET endpoint to retrieve the workflow state:

   ```csharp
   app.MapGet("/status/{instanceId}", async (
       [FromRoute] string instanceId,
       [FromServices] DaprWorkflowClient workflowClient) =>
   {
       var state = await workflowClient.GetWorkflowStateAsync(instanceId);
       if (state == null)
       {
           return Results.NotFound();
       }
       var output = state.ReadOutputAs<FirstWorkflow.Output>();
       return Results.Ok(new {instanceId, state, output});
   });
   ```

5. Add the POST and GET requests to the `CatalystAspireApp.ApiService/CatalystAspireApp.ApiService.http` file for later use:

   ```http
   ### Start a workflow
   # @name startRequest
   POST {{ApiService_HostAddress}}/start

   ### Get the workflow status
   @instanceId = {{startRequest.response.body.$.instanceId}}
   GET {{ApiService_HostAddress}}/status/{{instanceId}}
   ```

   Alternatively you can use these cURL statements:

   ```shell
   curl --request POST --url http://<apiservice-host>:<port>/start
   curl --request GET --url http://<apiservice-host>:<port>/status/<instanceID>
   ```

## 6. Run locally with Dapr

1. Start up the Aspire application in the root of the solution:

   ```shell
   aspire run
   ```

2. Once the application is up and running, use the POST `start` endpoint in `CatalystAspireApp.ApiService/CatalystAspireApp.ApiService.http` to start a workflow.

3. Use the GET `status` endpoint in `CatalystAspireApp.ApiService/CatalystAspireApp.ApiService.http` to verify the workflow state.

### 6.1 Aspire dashboard

To inspect the traces of the workflow execution open the *Traces* tab of the Aspire Dashboard and select the *apiservice: POST/start* trace:

![Aspire Traces](https://docs.diagrid.io/img/catalyst/dotnet/dotnet-aspire-traces.png)

### 6.2 Dapr Dev Dashboard

To inspect the workflow execution history open the Dapr Dev Dashboard via the Aspire Resources tab. Select the URL of the `diagrid-dashboard` resource:

![Aspire Resources for Dapr Dev Dashboard](https://docs.diagrid.io/img/catalyst/dotnet/dotnet-aspire-resources.png)

Once the Dapr Dev Dashboard is opened, select the Workflows tab in the menu to see all workflow executions. Select the workflow instance ID to drill down and see the workflow inputs/outputs and execution history:

![Dapr Dev Dashboard](https://docs.diagrid.io/img/catalyst/dotnet/diagrid-dev-dashboard.png)

## 7. Switch to Catalyst

Once you're done with local development and want to get more insights on how your workflow is performing, you can switch the local Dapr workflow engine with the workflow engine managed by Catalyst.

1. Add the [Diagrid Catalyst Aspire integration](https://www.nuget.org/packages/Diagrid.Aspire.Hosting.Catalyst) package to the AppHost project:

   ```shell
   dotnet add CatalystAspireApp.AppHost/CatalystAspireApp.AppHost.csproj package Diagrid.Aspire.Hosting.Catalyst
   ```

2. Replace the complete `CatalystAspireApp.AppHost/AppHost.cs` content with:

   ```csharp
   using Diagrid.Aspire.Hosting.Catalyst;
   using Microsoft.AspNetCore.Components.Authorization;

   var builder = DistributedApplication.CreateBuilder(args);

   // This configures a new project in Catalyst with a managed state store for workflow state.
   var catalystProject = builder.AddCatalystProject("catalyst-aspire")
       .WithCatalystKvStore();

   // The apiService will not use a Dapr sidecar anymore but will use the Catalyst.
   var apiService = builder.AddProject<Projects.CatalystAspireApp_ApiService>("apiservice")
       .WithCatalyst(catalystProject);

   // We're not using the web front-end project in this tutorial, but it can stay.
   builder.AddProject<Projects.CatalystAspireApp_Web>("webfrontend")
       .WithExternalHttpEndpoints()
       .WithHttpHealthCheck("/health")
       .WithReference(apiService)
       .WaitFor(apiService);

   builder.Build().Run();
   ```

3. These dependencies can now be removed from the AppHost project since the state is now managed by Diagrid Catalyst.

   ```shell
   dotnet remove ./CatalystAspireApp.AppHost package Aspire.Hosting.Valkey
   dotnet remove ./CatalystAspireApp.AppHost package CommunityToolkit.Aspire.Hosting.Dapr
   ```

## 8. Run with Diagrid Catalyst

1. Login to Diagrid Catalyst:

   ```bash
   diagrid login
   ```

2. Start the Aspire application from the root of the solution:

   ```bash
   aspire run
   ```

### 8.1 Aspire dashboard

Once the application is running, use the Aspire Resources tab to navigate to the Catalyst Dashboard:

![Aspire Resources for Catalyst Dashboard](https://docs.diagrid.io/img/catalyst/dotnet/aspire-resources-catalyst.png)

### 8.2 Catalyst dashboard

1. In Catalyst, use the *Workflows* tab to navigate to the workflow view:

   ![Catalyst Workflows](https://docs.diagrid.io/img/catalyst/dotnet/catalyst-workflows.png)

2. Click on the *FirstWorkflow* to drill down:

   ![Catalyst Workflow Types](https://docs.diagrid.io/img/catalyst/dotnet/catalyst-workflow-type.png)

3. Click on a workflow execution to see the inputs/outputs, interactive graph and execution history:

   ![Catalyst Workflow Detail](https://docs.diagrid.io/img/catalyst/dotnet/catalyst-workflow-detail.png)

### 8.3 Troubleshooting

If you're running into issues with the Diagrid Catalyst Aspire integration (e.g. unresponsive status in the Aspire dashboard) run the following command to stop the local dev tunnel for the project:

```shell
diagrid dev stop --project catalyst-aspire
```

Once the dev tunnel is stopped retry running the Aspire solution again.

## Summary

In this tutorial you:

- Scaffolded an Aspire starter application and added Dapr Workflow support
- Created a workflow with two activities that perform sequential operations
- Added HTTP endpoints to start and monitor workflow executions
- Ran the application locally using the Dapr sidecar and Valkey for state storage
- Inspected workflow traces in the Aspire Dashboard and the Dapr Dev Dashboard
- Switched from the local Dapr workflow engine to the managed Catalyst workflow engine
- Monitored workflow executions, inputs/outputs, and execution history in the Catalyst Dashboard

## Next steps

- Explore the [Dapr workflow patterns](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-patterns/) such as fan-out/fan-in, chaining, and sub-workflows.

**Dapr & AI University — Learn this hands-on:** [Build Dapr workflows in .NET with Aspire](https://www.diagrid.io/university/dapr-workflows-dotnet-aspire?utm_source=docs&utm_medium=cta&utm_campaign=dapr-workflows-dotnet-aspire)

Build fault-tolerant Dapr Workflow applications in .NET, orchestrated and run locally with .NET Aspire.

Advanced · 30 min · .NET
