Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
59b2020
feat(dapr): add WaitFor support for Dapr sidecar resources
gabynevada Aug 26, 2025
6e67899
feat(dapr): add WithMetadata overload for EndpointReference
gabynevada Aug 26, 2025
807f941
feat: add value provider implementation
gabynevada Aug 26, 2025
757691a
feat: test using secrets with envs
gabynevada Aug 26, 2025
1cc5328
chore: add test for dapr pubsub
gabynevada Aug 26, 2025
4bd42cc
chore: use simpler endpoint prefix
gabynevada Aug 26, 2025
cc291a6
feat: replace overload with IValueProvider for flexibility
gabynevada Aug 26, 2025
a5e5a75
chore: use target port for example
gabynevada Aug 26, 2025
60b2b1b
chore: remove test method
gabynevada Aug 26, 2025
e6c6031
feat: simplify logic by avoiding secret placeholders
gabynevada Aug 26, 2025
7e3e218
chore: make configuration example cleaner
gabynevada Aug 26, 2025
4fda6b0
refactor: consolidate value provider tests and remove placeholder pat…
gabynevada Aug 26, 2025
744076a
chore: remove unused comments and unnecesarry tests
gabynevada Aug 26, 2025
fe25c66
chore: remove unused using
gabynevada Aug 26, 2025
e409ed1
feat: enhance Dapr sidecar lifecycle management and metadata configur…
gabynevada Aug 26, 2025
a31d1fe
feat(dapr): add DaprComponent wait lifecycle implementation
gabynevada Aug 26, 2025
50a9567
feat(dapr): add component dependency wait support via SetupComponentL…
gabynevada Aug 26, 2025
f839ea0
chore: remove providers for component lyfecycle
gabynevada Aug 26, 2025
93d851a
feat: pass the wait annotations from the configuration resource to th…
gabynevada Aug 26, 2025
1566eda
chore: add waitfor for dapr state store to redis
gabynevada Aug 26, 2025
25242ce
refactor(dapr): use component lifetimes instead of passing wait annot…
gabynevada Aug 30, 2025
e27b40e
fix(dapr): use WaitUntilHealthy for component dependencies
gabynevada Aug 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,37 @@
var redis = builder.AddRedis("redis").WithRedisInsight();


var stateStore = builder.AddDaprStateStore("statestore");

var pubSub = builder.AddDaprPubSub("pubsub")
.WithMetadata("redisHost", "localhost:6379")
.WaitFor(redis);
var stateStore = builder.AddDaprStateStore("statestore")
.WaitFor(redis);

var redisHost= redis.Resource.PrimaryEndpoint.Property(EndpointProperty.Host);
var redisTargetPort = redis.Resource.PrimaryEndpoint.Property(EndpointProperty.TargetPort);

var pubSub = builder
.AddDaprPubSub("pubsub")
.WithMetadata(
"redisHost",
ReferenceExpression.Create(
$"{redisHost}:{redisTargetPort}"
)
)
.WaitFor(redis);

builder.AddProject<Projects.CommunityToolkit_Aspire_Hosting_Dapr_ServiceA>("servicea")
.WithReference(stateStore)
.WithReference(pubSub)
.WithDaprSidecar()
.WaitFor(redis);
.WaitFor(pubSub);

builder.AddProject<Projects.CommunityToolkit_Aspire_Hosting_Dapr_ServiceB>("serviceb")
.WithReference(pubSub)
.WithDaprSidecar()
.WaitFor(redis);
.WaitFor(pubSub);

// console app with no appPort (sender only)
builder.AddProject<Projects.CommunityToolkit_Aspire_Hosting_Dapr_ServiceC>("servicec")
.WithReference(stateStore)
.WithDaprSidecar()
.WaitFor(redis);
.WaitFor(stateStore);

builder.Build().Run();
2 changes: 1 addition & 1 deletion src/Shared/Dapr/Core/DaprComponentMetadataAnnotation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Aspire.Hosting.ApplicationModel;

namespace CommunityToolkit.Aspire.Hosting.Dapr;
internal sealed record DaprComponentConfigurationAnnotation(Func<DaprComponentSchema, Task> Configure) : IResourceAnnotation;
internal sealed record DaprComponentConfigurationAnnotation(Func<DaprComponentSchema, CancellationToken, Task> Configure) : IResourceAnnotation;
40 changes: 39 additions & 1 deletion src/Shared/Dapr/Core/DaprComponentSchema.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using YamlDotNet.Serialization;
using Aspire.Hosting.ApplicationModel;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace CommunityToolkit.Aspire.Hosting.Dapr;
Expand Down Expand Up @@ -29,6 +30,20 @@ public DaprComponentSchema(string name, string type)
};
}
public override string ToString() => serializer.Serialize(this);

/// <summary>
/// Resolves all async values in the component schema
/// </summary>
public async Task ResolveAllValuesAsync(CancellationToken cancellationToken = default)
{
foreach (var metadata in Spec.Metadata)
{
if (metadata is DaprComponentSpecMetadataValueProvider valueProvider)
{
await valueProvider.ResolveValueAsync(cancellationToken);
}
}
}

public static DaprComponentSchema FromYaml(string yamlContent) =>
deserializer.Deserialize<DaprComponentSchema>(yamlContent);
Expand Down Expand Up @@ -104,6 +119,29 @@ public sealed class DaprComponentSpecMetadataValue : DaprComponentSpecMetadata
public required string Value { get; set; }
}

Copy link

Copilot AI Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This internal class lacks XML documentation comments. As it's a key component for value provider functionality, it should have proper documentation explaining its purpose and usage.

Suggested change
/// <summary>
/// Represents a Dapr component spec metadata item whose value is provided by an <see cref="IValueProvider"/> and can be resolved asynchronously.
/// </summary>

Copilot uses AI. Check for mistakes.
internal sealed class DaprComponentSpecMetadataValueProvider : DaprComponentSpecMetadata
{
/// <summary>
/// The value provider for deferred evaluation
/// </summary>
[YamlIgnore]
public required IValueProvider ValueProvider { get; init; }

/// <summary>
/// The resolved value (populated after resolution)
/// </summary>
[YamlMember(Order = 2)]
public string? Value { get; set; }

/// <summary>
/// Resolves the value from the provider
/// </summary>
public async Task ResolveValueAsync(CancellationToken cancellationToken = default)
{
Value = await ValueProvider.GetValueAsync(cancellationToken) ?? string.Empty;
}
}

/// <summary>
/// Represents a Dapr component spec metadata item with a secret key reference
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Shared/Dapr/Core/DaprComponentValueProviderAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Aspire.Hosting.ApplicationModel;

namespace CommunityToolkit.Aspire.Hosting.Dapr;

/// <summary>
/// Annotation that tracks value providers that need deferred resolution for Dapr component metadata
/// </summary>
internal sealed record DaprComponentValueProviderAnnotation(string MetadataName, string EnvironmentVariableName, IValueProvider ValueProvider) : IResourceAnnotation;
122 changes: 96 additions & 26 deletions src/Shared/Dapr/Core/DaprDistributedApplicationLifecycleHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -32,6 +34,10 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
{
string appHostDirectory = GetAppHostDirectory();

// Set up WaitAnnotations for Dapr components based on their value provider dependencies
SetupComponentLifecycle(appModel);


var onDemandResourcesPaths = await StartOnDemandDaprComponentsAsync(appModel, cancellationToken).ConfigureAwait(false);

var sideCars = new List<ExecutableResource>();
Expand Down Expand Up @@ -71,35 +77,41 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell

var componentReferenceAnnotations = resource.Annotations.OfType<DaprComponentReferenceAnnotation>();

var waitAnnotationsToCopyToDaprCli = new List<WaitAnnotation>();

var secrets = new Dictionary<string, string>();
var endpointEnvironmentVars = new Dictionary<string, IValueProvider>();
var hasValueProviders = false;

foreach (var componentReferenceAnnotation in componentReferenceAnnotations)
{
// Check if there are any value provider references that need to be added as environment variables
if (componentReferenceAnnotation.Component.TryGetAnnotationsOfType<DaprComponentValueProviderAnnotation>(out var endpointAnnotations))
{
foreach (var endpointAnnotation in endpointAnnotations)
{
endpointEnvironmentVars[endpointAnnotation.EnvironmentVariableName] = endpointAnnotation.ValueProvider;
hasValueProviders = true;
}
}

// Check if there are any secrets that need to be added to the secret store
if (componentReferenceAnnotation.Component.TryGetAnnotationsOfType<DaprComponentSecretAnnotation>(out var secretAnnotations))
{
foreach (var secretAnnotation in secretAnnotations)
{
secrets[secretAnnotation.Key] = (await secretAnnotation.Value.GetValueAsync(cancellationToken))!;
}
// We need to append the secret store path to the resources path
onDemandResourcesPaths.TryGetValue("secretstore", out var secretStorePath);
}

// If we have any secrets or value providers, ensure the secret store path is added
if ((secrets.Count > 0 || hasValueProviders) && onDemandResourcesPaths.TryGetValue("secretstore", out var secretStorePath))
{
string onDemandResourcesPathDirectory = Path.GetDirectoryName(secretStorePath)!;

if (onDemandResourcesPathDirectory is not null)
{
aggregateResourcesPaths.Add(onDemandResourcesPathDirectory);
}
}

// Whilst we are passing over each component annotations collect the list of annotations to copy to the Dapr CLI.
if (componentReferenceAnnotation.Component.TryGetAnnotationsOfType<WaitAnnotation>(out var componentWaitAnnotations))
{
waitAnnotationsToCopyToDaprCli.AddRange(componentWaitAnnotations);
}

if (componentReferenceAnnotation.Component.Options?.LocalPath is not null)
{
var localPathDirectory = Path.GetDirectoryName(NormalizePath(componentReferenceAnnotation.Component.Options.LocalPath));
Expand All @@ -120,19 +132,23 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
}
}

if (secrets.Count > 0)
if (secrets.Count > 0 || endpointEnvironmentVars.Count > 0)
{
daprSidecar.Annotations.Add(new EnvironmentCallbackAnnotation(context =>
daprSidecar.Annotations.Add(new EnvironmentCallbackAnnotation(async context =>
{
foreach (var secret in secrets)
{
context.EnvironmentVariables.TryAdd(secret.Key, secret.Value);
}


// Add value provider references
foreach (var (envVarName, valueProvider) in endpointEnvironmentVars)
{
var value = await valueProvider.GetValueAsync(context.CancellationToken);
context.EnvironmentVariables.TryAdd(envVarName, value ?? string.Empty);
}
Comment on lines +137 to +149
Copy link

Copilot AI Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EnvironmentCallbackAnnotation constructor expects a synchronous delegate, but an async lambda is being passed. This could lead to fire-and-forget behavior where exceptions are swallowed. Consider using a synchronous approach or ensure proper exception handling.

Suggested change
daprSidecar.Annotations.Add(new EnvironmentCallbackAnnotation(async context =>
{
foreach (var secret in secrets)
{
context.EnvironmentVariables.TryAdd(secret.Key, secret.Value);
}
// Add value provider references
foreach (var (envVarName, valueProvider) in endpointEnvironmentVars)
{
var value = await valueProvider.GetValueAsync(context.CancellationToken);
context.EnvironmentVariables.TryAdd(envVarName, value ?? string.Empty);
}
// Precompute all value provider results asynchronously
var precomputedEnvVars = new Dictionary<string, string>();
foreach (var secret in secrets)
{
precomputedEnvVars[secret.Key] = secret.Value;
}
foreach (var (envVarName, valueProvider) in endpointEnvironmentVars)
{
// Synchronously block here to get the value
// (since we're already in an async method, this is safe)
var value = valueProvider.GetValueAsync(cancellationToken).GetAwaiter().GetResult();
precomputedEnvVars[envVarName] = value ?? string.Empty;
}
daprSidecar.Annotations.Add(new EnvironmentCallbackAnnotation(context =>
{
foreach (var kvp in precomputedEnvVars)
{
context.EnvironmentVariables.TryAdd(kvp.Key, kvp.Value);
}

Copilot uses AI. Check for mistakes.
}));
}
// It is possible that we have duplicate wate annotations so we just dedupe them here.
var distinctWaitAnnotationsToCopyToDaprCli = waitAnnotationsToCopyToDaprCli.DistinctBy(w => (w.Resource, w.WaitType));

var daprAppPortArg = (int? port) => ModelNamedArg("--app-port", port);
var daprGrpcPortArg = (object port) => ModelNamedObjectArg("--dapr-grpc-port", port);
Expand Down Expand Up @@ -183,8 +199,11 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
var daprCliResourceName = $"{daprSidecar.Name}-cli";
var daprCli = new ExecutableResource(daprCliResourceName, fileName, appHostDirectory);

// Add all the unique wait annotations to the CLI.
daprCli.Annotations.AddRange(distinctWaitAnnotationsToCopyToDaprCli);
// Make the Dapr CLI wait for the component resources it references
foreach (var componentRef in componentReferenceAnnotations)
{
daprCli.Annotations.Add(new WaitAnnotation(componentRef.Component, WaitType.WaitUntilHealthy));
}

resource.Annotations.Add(
new EnvironmentCallbackAnnotation(
Expand Down Expand Up @@ -416,6 +435,51 @@ static IEnumerable<string> GetAvailablePaths()
}
}

private static void SetupComponentLifecycle(DistributedApplicationModel appModel)
{
// Setup proper lifecycle for Dapr components with their dependencies
// Components will manage their own state and wait for their dependencies
foreach (var component in appModel.Resources.OfType<IDaprComponentResource>())
{
var dependencies = new HashSet<IResource>();

if (component.TryGetAnnotationsOfType<DaprComponentValueProviderAnnotation>(out var valueProviderAnnotations))
{
foreach (var annotation in valueProviderAnnotations)
{
// Extract resource references from value providers - following the same pattern as SetupSidecarLifecycle
if (annotation.ValueProvider is IResourceWithoutLifetime)
{
// Skip waiting for resources without a lifetime
continue;
}

if (annotation.ValueProvider is IResource resource)
{
dependencies.Add(resource);
}
else if (annotation.ValueProvider is IValueWithReferences valueWithReferences)
{
foreach (var innerRef in valueWithReferences.References.OfType<IResource>())
{
if (innerRef is not IResourceWithoutLifetime)
{
dependencies.Add(innerRef);
}
}
}
}
}

// Add WaitAnnotations for each unique dependency
// This ensures the component waits for its dependencies before becoming ready
foreach (var dependency in dependencies)
{
component.Annotations.Add(new WaitAnnotation(dependency, WaitType.WaitUntilHealthy));
}
}
}

public void Dispose()
{
if (_onDemandResourcesRootPath is not null)
Expand All @@ -442,8 +506,12 @@ private async Task<IReadOnlyDictionary<string, string>> StartOnDemandDaprCompone
.Where(component => component.Options?.LocalPath is null)
.ToList();

// If any of the components have secrets, we will add an on-demand secret store component.
if (onDemandComponents.Any(component => component.TryGetAnnotationsOfType<DaprComponentSecretAnnotation>(out var annotations) && annotations.Any()))
// If any of the components have secrets or value provider references, we will add an on-demand secret store component.
bool needsSecretStore = onDemandComponents.Any(component =>
(component.TryGetAnnotationsOfType<DaprComponentSecretAnnotation>(out var secretAnnotations) && secretAnnotations.Any()) ||
(component.TryGetAnnotationsOfType<DaprComponentValueProviderAnnotation>(out var valueProviderAnnotations) && valueProviderAnnotations.Any()));

if (needsSecretStore)
{
onDemandComponents.Add(new DaprComponentResource("secretstore", DaprConstants.BuildingBlocks.SecretStore));
}
Expand Down Expand Up @@ -492,7 +560,7 @@ private async Task<string> GetComponentAsync(DaprComponentResource component, Fu
{
// We should try to read content from a known location (such as aspire root directory)
logger.LogInformation("Unvalidated configuration {specType} for component '{ComponentName}'.", component.Type, component.Name);
return await contentWriter(await GetDaprComponent(component, component.Type)).ConfigureAwait(false);
return await contentWriter(await GetDaprComponent(component, component.Type, cancellationToken)).ConfigureAwait(false);
}
private async Task<string> GetBuildingBlockComponentAsync(DaprComponentResource component, Func<string, Task<string>> contentWriter, string defaultProvider, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -545,19 +613,21 @@ private static async Task<string> GetDefaultContent(DaprComponentResource compon
string defaultContent = await File.ReadAllTextAsync(defaultContentPath, cancellationToken).ConfigureAwait(false);
string yaml = defaultContent.Replace($"name: {component.Type}", $"name: {component.Name}");
DaprComponentSchema content = DaprComponentSchema.FromYaml(yaml);
await ConfigureDaprComponent(component, content);
await ConfigureDaprComponent(component, content, cancellationToken);
await content.ResolveAllValuesAsync(cancellationToken);
return content.ToString();
}


private static async Task<string> GetDaprComponent(DaprComponentResource component, string type)
private static async Task<string> GetDaprComponent(DaprComponentResource component, string type, CancellationToken cancellationToken = default)
{
var content = new DaprComponentSchema(component.Name, type);
await ConfigureDaprComponent(component, content);
await ConfigureDaprComponent(component, content, cancellationToken);
await content.ResolveAllValuesAsync(cancellationToken);
return content.ToString();
}

private static async Task ConfigureDaprComponent(DaprComponentResource component, DaprComponentSchema content)
private static async Task ConfigureDaprComponent(DaprComponentResource component, DaprComponentSchema content, CancellationToken cancellationToken = default)
{
if (component.TryGetAnnotationsOfType<DaprComponentSecretAnnotation>(out var secrets) && secrets.Any())
{
Expand All @@ -567,7 +637,7 @@ private static async Task ConfigureDaprComponent(DaprComponentResource component
{
foreach (var annotation in annotations)
{
await annotation.Configure(content);
await annotation.Configure(content, cancellationToken);
}
}
}
Expand Down
Loading
Loading