CommunityToolkit / CommunityToolkit/Aspire

Resource Management - Dapr & Aspire

Open
#1,576 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
627
Forks
196
Avg merge
2d 11h
Merged PRs (30d)
35

Description

### Describe the bug

CommunityToolkit.Aspire.Hosting.Dapr Version=13.0.0

I tried following 3 edge cases:

1. When dependencies are managed outside of Aspire

`
using CommunityToolkit.Aspire.Hosting.Dapr;

var builder = DistributedApplication.CreateBuilder(args);

// The components directory is where the Dapr components will be placed for the Dapr sidecar to consume.
var componentsPath = Path.Combine(builder.AppHostDirectory, "components");

// The YAML file points to an external state store instance
var stateStore = builder.AddDaprStateStore("statestore", new DaprComponentOptions
{
// Path to your Dapr component YAML file - PostgreSQL state store
LocalPath = Path.Combine(componentsPath, "statestore.yaml")
});

// API Service
var apiService = builder.AddProject("apiservice")
.WithHttpHealthCheck("/health")
.WithDaprSidecar(sidecar => sidecar
.WithOptions(new DaprSidecarOptions
{
AppId = "apiservice",
DaprHttpPort = 3500
})
.WithReference(stateStore));

builder.Build().Run();
`

Observations:
- Incomplete resource visibility on Aspire dashboard e.g., PostgreSQL is not available on dashboard
- Add cognitive overhead on developer to know the dependencies and it may not get all benefits of Aspire deployment

2. When dependencies are managed by Aspire

`
using CommunityToolkit.Aspire.Hosting.Dapr;

var builder = DistributedApplication.CreateBuilder(args);

// The components directory is where the Dapr components will be placed for the Dapr sidecar to consume.
var componentsPath = Path.Combine(builder.AppHostDirectory, "components");

// PostgreSQL
var postgres = builder.AddPostgres("postgres")
.WithImageTag("17.6")
.WithContainerName("aspiredapr-demo-postgres")
.WithDataVolume("aspiredapr-demo-postgres-data")
.WithLifetime(ContainerLifetime.Persistent);

var appDb = postgres.AddDatabase("appdb");

// Get the endpoint information for the PostgreSQL component to use in the Dapr component configuration
var postgresEndpoint = postgres.GetEndpoint("tcp");

var stateStore = builder.AddDaprComponent(
"statestore",
"state.postgresql",
new DaprComponentOptions
{
LocalPath = Path.Combine(componentsPath, "statestore.yaml")
})
.WithMetadata("host", postgresEndpoint.Property(EndpointProperty.Host))
.WithMetadata("port", postgresEndpoint.Property(EndpointProperty.Port))
.WithMetadata("database", appDb.Resource.DatabaseName)
.WithMetadata("user", postgres.Resource.UserNameReference)
.WithMetadata("password", postgres.Resource.PasswordParameter!);

// API Service
var apiService = builder.AddProject("apiservice")
.WithReference(appDb)
.WaitFor(postgres)
.WaitFor(appDb)
.WithHttpHealthCheck("/health")
.WithDaprSidecar(sidecar => sidecar
.WithOptions(new DaprSidecarOptions
{
AppId = "apiservice",
DaprHttpPort = 3500
})
.WithReference(stateStore)
.WaitFor(postgres)
.WaitFor(appDb));

builder.Build().Run();
`
Observations:
- Race condition on first run e.g., application fails on first run as the Dapr Sidecar does not wait for dependent resources to reach a ready state before proceeding
- Without a built-in readiness mechanism, the Dapr Sidecar start independently, make the first run unreliable but on subsequent run, it runs fine and the dashboard displays all resource properly

3. When dependencies are managed by Aspire & Explicit Dapr Sidecar

`
var builder = DistributedApplication.CreateBuilder(args);

// The components directory is where the Dapr components will be placed for the Dapr sidecar to consume.
var componentsPath = Path.Combine(builder.AppHostDirectory, "components");
const string appDatabaseName = "appdb";
const string postgresContainerName = "aspiredapr-demo-postgres";

// PostgreSQL
var postgres = builder.AddPostgres("postgres")
.WithImageTag("17.6")
.WithEnvironment("POSTGRES_DB", appDatabaseName)
.WithContainerName(postgresContainerName)
.WithDataVolume("aspiredapr-demo-postgres-data")
.WithLifetime(ContainerLifetime.Persistent);

var appDb = postgres.AddDatabase(appDatabaseName, appDatabaseName);

// Get the endpoint information for the PostgreSQL component to use in the Dapr component configuration
var postgresEndpoint = postgres.GetEndpoint("tcp");

var postgresReady = builder.AddExecutable(
"postgres-ready",
"sh",
builder.AppHostDirectory,
"-c",
"""
until nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do
sleep 1
done

until docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d postgres -v ON_ERROR_STOP=1 -c "select 1"' >/dev/null 2>&1; do
sleep 1
done

if ! docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d postgres -tAc "select 1 from pg_database where datname = '"'"'appdb'"'"'"' | grep -q 1; then
docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d postgres -v ON_ERROR_STOP=1 -c "create database appdb"'
fi

until docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d appdb -v ON_ERROR_STOP=1 -c "select 1"' >/dev/null 2>&1; do
sleep 1
done
""")
.WithEnvironment("POSTGRES_HOST", "127.0.0.1")
.WithEnvironment("POSTGRES_PORT", postgresEndpoint.Property(EndpointProperty.Port))
.WaitFor(postgres);

// API Service
var apiService = builder.AddProject("apiservice")
.WithReference(appDb)
.WithEnvironment("DAPR_HTTP_PORT", "3500")
.WithEnvironment("DAPR_GRPC_PORT", "50001")
.WaitForCompletion(postgresReady)
.WithHttpHealthCheck("/health");

var apiEndpoint = apiService.GetEndpoint("http");

builder.AddExecutable(
"apiservice-dapr",
"dapr",
builder.AppHostDirectory,
"run",
"--app-id",
"apiservice",
"--resources-path",
componentsPath,
"--app-port",
apiEndpoint.Property(EndpointProperty.Port),
"--dapr-http-port",
"3500",
"--dapr-grpc-port",
"50001",
"--app-channel-address",
"localhost",
"--app-protocol",
"http")
.WithEnvironment("STATESTORE_HOST", "127.0.0.1")
.WithEnvironment("STATESTORE_PORT", postgresEndpoint.Property(EndpointProperty.Port))
.WithEnvironment("STATESTORE_USER", postgres.Resource.UserNameReference)
.WithEnvironment("postgres-password", postgres.Resource.PasswordParameter!)
.WaitForCompletion(postgresReady)
.WaitFor(apiService);

builder.Build().Run();
`

Observations:
This approach is not using CommunityToolkit.Aspire.Hosting.Dapr. Here Dapr runs as an explicit executable and additonal code needed to check the readiness check. It allows Aspire to properly sequence the startup order and dashboard look OK but not perfect.
- Excessive custom code
- Cluttered Aspire dashboard

### Regression

_No response_

### Steps to reproduce

```text
.NET Version 10
Aspire version 13.4.0
```

### Expected behavior

I would suggest to make fix for 2. When dependencies are managed by Aspire

The Dapr Sidecar should wait for dependent resources readiness.

### Screenshots

_No response_

### IDE and version

VS Code

### IDE version

_No response_

### Nuget packages

```text
CommunityToolkit.Aspire.Hosting.Dapr Version=13.0.0
```

### Additional context

_No response_

### Help us help you

No, just wanted to report this

Contributor guide

Open the contributing guide

Research direction

Start by locating the CommunityToolkit.Aspire.Hosting.Dapr implementation behind AddDaprComponent and WithDaprSidecar, then reproduce the first-run startup with .NET 10, Aspire 13.4.0, and PostgreSQL. Done means the Dapr sidecar waits for dependent Aspire resources to become ready before starting, without requiring the explicit executable and custom readiness checks shown in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, docker, postgresql
Domain
backend, devops
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.