diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs index 4b3649423..45eb777c4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Net.Sockets; using static CommunityToolkit.Aspire.Hosting.Dapr.CommandLineArgs; @@ -240,8 +239,8 @@ private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken var http = daprCli.GetEndpoint("http"); var grpc = daprCli.GetEndpoint("grpc"); - context.EnvironmentVariables.TryAdd("DAPR_HTTP_PORT", http.Port.ToString(CultureInfo.InvariantCulture)); - context.EnvironmentVariables.TryAdd("DAPR_GRPC_PORT", grpc.Port.ToString(CultureInfo.InvariantCulture)); + context.EnvironmentVariables.TryAdd("DAPR_HTTP_PORT", http.Property(EndpointProperty.Port)); + context.EnvironmentVariables.TryAdd("DAPR_GRPC_PORT", grpc.Property(EndpointProperty.Port)); context.EnvironmentVariables.TryAdd("DAPR_GRPC_ENDPOINT", grpc); context.EnvironmentVariables.TryAdd("DAPR_HTTP_ENDPOINT", http); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Tests/DaprAzureContainerAppEnvironmentTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Tests/DaprAzureContainerAppEnvironmentTests.cs new file mode 100644 index 000000000..a184ee963 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Tests/DaprAzureContainerAppEnvironmentTests.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; +using Aspire.Hosting.Utils; +using CommunityToolkit.Aspire.Hosting.Dapr; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.Aspire.Hosting.Azure.Dapr.Tests; + +public class DaprAzureContainerAppEnvironmentTests +{ + // End-to-end guard for microsoft/aspire#18242 in the topology that triggers it: + // AddAzureContainerAppEnvironment (which contributes the azure-prepare-resources walk) plus a + // Dapr sidecar. Invokes the preparer's core operation directly so the test needs no Docker or + // provisioning; the non-Azure Dapr.Tests test is the primary deterministic guard. + [Fact] + public async Task PrepareResourcesWalk_DoesNotThrow_WithAzureEnvironmentAndDaprSidecar() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + // Fake path so the Dapr lifecycle hook does not fail locating the Dapr CLI. + builder.AddDapr(o => o.DaprPath = "dapr"); + builder.AddAzureContainerAppEnvironment("cae"); + + var worker = builder.AddContainer("worker", "image") + .WithDaprSidecar(); + + using var app = builder.Build(); + + await ExecuteBeforeStartHooksAsync(app, default); + + var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); + + var exception = await Record.ExceptionAsync(() => + worker.Resource.GetResourceDependenciesAsync(executionContext, ResourceDependencyDiscoveryMode.DirectOnly)); + + Assert.Null(exception); + } + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")] + private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken); +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/DaprSidecarEndpointAllocationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/DaprSidecarEndpointAllocationTests.cs new file mode 100644 index 000000000..71b8b2ef7 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/DaprSidecarEndpointAllocationTests.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; +using Aspire.Hosting.Utils; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.Tests; + +public class DaprSidecarEndpointAllocationTests +{ + // Regression test for microsoft/aspire#18242: the Dapr env callback must not eagerly read the + // sidecar CLI endpoint Port before allocation. GetResourceDependenciesAsync(DirectOnly) is the + // operation AzureResourcePreparer runs per compute resource, and reproduces the crash without Azure. + [Fact] + public async Task AppEnvironmentCallback_DoesNotThrow_WhenDaprSidecarEndpointsNotAllocated() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + // Fake path so the lifecycle hook does not fail locating the Dapr CLI. + builder.AddDapr(o => o.DaprPath = "dapr"); + + var container = builder.AddContainer("worker", "image") + .WithDaprSidecar(); + + using var app = builder.Build(); + + // Runs the Dapr hook (creates "worker-dapr-cli" and attaches the callback); endpoints stay unallocated. + await ExecuteBeforeStartHooksAsync(app, default); + + var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); + + var dependencies = await container.Resource.GetResourceDependenciesAsync( + executionContext, + ResourceDependencyDiscoveryMode.DirectOnly); + + Assert.Contains(dependencies, r => r.Name == "worker-dapr-cli"); + + // Fire the callback directly (do not resolve - a deferred port blocks until allocation) and + // assert the ports are recorded as deferred providers rather than eagerly-read values. + var callbackContext = new EnvironmentCallbackContext(executionContext, container.Resource); + foreach (var callback in container.Resource.Annotations.OfType()) + { + await callback.Callback(callbackContext); + } + + Assert.True(callbackContext.EnvironmentVariables.ContainsKey("DAPR_HTTP_PORT")); + Assert.True(callbackContext.EnvironmentVariables.ContainsKey("DAPR_GRPC_PORT")); + Assert.IsAssignableFrom(callbackContext.EnvironmentVariables["DAPR_HTTP_PORT"]); + Assert.IsAssignableFrom(callbackContext.EnvironmentVariables["DAPR_GRPC_PORT"]); + } + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")] + private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken); +}