Skip to content

Commit a5b6be8

Browse files
committed
Fix event order and callback calling during container creation
1 parent 046a63b commit a5b6be8

20 files changed

Lines changed: 1106 additions & 559 deletions

src/Aspire.Hosting/ApplicationModel/ArgumentsExecutionConfigurationGatherer.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,24 @@ internal class ArgumentsExecutionConfigurationGatherer : IExecutionConfiguration
1313
/// <inheritdoc/>
1414
public async ValueTask GatherAsync(IExecutionConfigurationGathererContext context, IResource resource, ILogger resourceLogger, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken = default)
1515
{
16-
if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var callbacks))
16+
if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var argumentAnnotations))
1717
{
18-
var callbackContext = new CommandLineArgsCallbackContext(context.Arguments, resource, cancellationToken)
18+
IList<object> args = [.. context.Arguments];
19+
var callbackContext = new CommandLineArgsCallbackContext(args, resource, cancellationToken)
1920
{
2021
Logger = resourceLogger,
2122
ExecutionContext = executionContext
2223
};
2324

24-
foreach (var callback in callbacks)
25+
foreach (var ann in argumentAnnotations)
2526
{
26-
await callback.Callback(callbackContext).ConfigureAwait(false);
27+
// Each annotation operates on a shared context.
28+
args = await ann.AsCallbackAnnotation().EvaluateOnceAsync(callbackContext).ConfigureAwait(false);
2729
}
30+
31+
// Take the final result and apply to the gatherer context.
32+
context.Arguments.Clear();
33+
context.Arguments.AddRange(args);
2834
}
2935
}
3036
}

src/Aspire.Hosting/ApplicationModel/CommandLineArgsCallbackAnnotation.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Immutable;
45
using Microsoft.Extensions.Logging;
56
using Microsoft.Extensions.Logging.Abstractions;
67

78
namespace Aspire.Hosting.ApplicationModel;
89

10+
using IArgCallbackAnnotation = ICallbackResourceAnnotation<CommandLineArgsCallbackContext, IList<object>>;
11+
912
/// <summary>
1013
/// Represents an annotation that provides a callback to be executed with a list of command-line arguments when an executable resource is started.
1114
/// </summary>
12-
public class CommandLineArgsCallbackAnnotation : IResourceAnnotation
15+
public class CommandLineArgsCallbackAnnotation : IResourceAnnotation, IArgCallbackAnnotation
1316
{
17+
private Task<IList<object>>? _callbackTask;
18+
private readonly object _lock = new();
19+
1420
/// <summary>
1521
/// Initializes a new instance of the <see cref="CommandLineArgsCallbackAnnotation"/> class with the specified callback action.
1622
/// </summary>
@@ -41,6 +47,35 @@ public CommandLineArgsCallbackAnnotation(Action<IList<object>> callback)
4147
/// Gets the callback action to be executed when the executable arguments are parsed.
4248
/// </summary>
4349
public Func<CommandLineArgsCallbackContext, Task> Callback { get; }
50+
51+
internal IArgCallbackAnnotation AsCallbackAnnotation() => this;
52+
53+
Task<IList<object>> IArgCallbackAnnotation.EvaluateOnceAsync(CommandLineArgsCallbackContext context)
54+
{
55+
lock(_lock)
56+
{
57+
if (_callbackTask is null)
58+
{
59+
_callbackTask = ExecuteCallbackAsync(context);
60+
}
61+
return _callbackTask;
62+
}
63+
}
64+
65+
void IArgCallbackAnnotation.ForgetCachedResult()
66+
{
67+
lock(_lock)
68+
{
69+
_callbackTask = null;
70+
}
71+
}
72+
73+
private async Task<IList<object>> ExecuteCallbackAsync(CommandLineArgsCallbackContext context)
74+
{
75+
await Callback(context).ConfigureAwait(false);
76+
var result = context.Args.ToImmutableList();
77+
return result;
78+
}
4479
}
4580

4681
/// <summary>

src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackAnnotation.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55

66
namespace Aspire.Hosting.ApplicationModel;
77

8+
using IEnvCallbackAnnotation = ICallbackResourceAnnotation<EnvironmentCallbackContext, Dictionary<string, object>>;
9+
810
/// <summary>
911
/// Represents an annotation that provides a callback to modify the environment variables of an application.
1012
/// </summary>
1113
[DebuggerDisplay("{DebuggerToString(),nq}")]
12-
public class EnvironmentCallbackAnnotation : IResourceAnnotation
14+
public class EnvironmentCallbackAnnotation : IResourceAnnotation, IEnvCallbackAnnotation
1315
{
1416
private readonly string? _name;
17+
private Task<Dictionary<string, object>>? _callbackTask;
18+
private readonly object _lock = new();
1519

1620
/// <summary>
1721
/// Initializes a new instance of the <see cref="EnvironmentCallbackAnnotation"/> class with the specified name and callback function.
@@ -77,6 +81,35 @@ public EnvironmentCallbackAnnotation(Func<EnvironmentCallbackContext, Task> call
7781
/// </summary>
7882
public Func<EnvironmentCallbackContext, Task> Callback { get; private set; }
7983

84+
internal IEnvCallbackAnnotation AsCallbackAnnotation() => this;
85+
86+
Task<Dictionary<string, object>> IEnvCallbackAnnotation.EvaluateOnceAsync(EnvironmentCallbackContext context)
87+
{
88+
lock(_lock)
89+
{
90+
if (_callbackTask is null)
91+
{
92+
_callbackTask = ExecuteCallbackAsync(context);
93+
}
94+
return _callbackTask;
95+
}
96+
}
97+
98+
void IEnvCallbackAnnotation.ForgetCachedResult()
99+
{
100+
lock(_lock)
101+
{
102+
_callbackTask = null;
103+
}
104+
}
105+
106+
private async Task<Dictionary<string, object>> ExecuteCallbackAsync(EnvironmentCallbackContext context)
107+
{
108+
await Callback(context).ConfigureAwait(false);
109+
var result = new Dictionary<string, object>(context.EnvironmentVariables);
110+
return result;
111+
}
112+
80113
private string DebuggerToString()
81114
{
82115
var text = $@"Type = {GetType().Name}";

src/Aspire.Hosting/ApplicationModel/EnvironmentVariablesConfigurationGatherer.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,24 @@ internal class EnvironmentVariablesExecutionConfigurationGatherer : IExecutionCo
1313
/// <inheritdoc/>
1414
public async ValueTask GatherAsync(IExecutionConfigurationGathererContext context, IResource resource, ILogger resourceLogger, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken = default)
1515
{
16-
if (resource.TryGetEnvironmentVariables(out var callbacks))
16+
if (resource.TryGetEnvironmentVariables(out var envVarAnnotations))
1717
{
18-
var callbackContext = new EnvironmentCallbackContext(executionContext, resource, context.EnvironmentVariables, cancellationToken)
18+
var envVars = new Dictionary<string, object>(context.EnvironmentVariables);
19+
var callbackContext = new EnvironmentCallbackContext(executionContext, resource, envVars, cancellationToken: cancellationToken)
1920
{
2021
Logger = resourceLogger,
2122
};
2223

23-
foreach (var callback in callbacks)
24+
foreach (var ann in envVarAnnotations)
2425
{
25-
await callback.Callback(callbackContext).ConfigureAwait(false);
26+
// Each annotation operates on a shared context.
27+
envVars = await ann.AsCallbackAnnotation().EvaluateOnceAsync(callbackContext).ConfigureAwait(false);
28+
}
29+
30+
// Take the final result and apply to the gatherer context.
31+
foreach (var kvp in envVars)
32+
{
33+
context.EnvironmentVariables[kvp.Key] = kvp.Value;
2634
}
2735
}
2836
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Hosting.ApplicationModel;
5+
6+
/// <summary>
7+
/// Represents a resource annotation whose callback should be evaluated at most once,
8+
/// with the result cached for subsequent retrievals.
9+
/// </summary>
10+
/// <typeparam name="TContext">The type of the context passed to the callback.</typeparam>
11+
/// <typeparam name="TResult">The type of the result produced by the callback.</typeparam>
12+
internal interface ICallbackResourceAnnotation<TContext, TResult>
13+
{
14+
/// <summary>
15+
/// Evaluates the callback if it has not been evaluated yet, caching the result.
16+
/// Subsequent calls return the cached result regardless of the context passed.
17+
/// </summary>
18+
/// <param name="context">The context for the callback evaluation. Only used on the first call.</param>
19+
/// <returns>The cached result of the callback evaluation.</returns>
20+
Task<TResult> EvaluateOnceAsync(TContext context);
21+
22+
/// <summary>
23+
/// Clears the cached result so that the next call to <see cref="EvaluateOnceAsync"/> will re-execute the callback.
24+
///</summary>
25+
/// <remarks>
26+
/// Use <see cref="ForgetCachedResult"/> when a resource decorated with this callback annotation is restarted.
27+
/// </remarks>
28+
void ForgetCachedResult();
29+
}

src/Aspire.Hosting/ApplicationModel/ResourceDependencyDiscoveryMode.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace Aspire.Hosting.ApplicationModel;
55

66
/// <summary>
7-
/// Specifies how resource dependencies are discovered.
7+
/// Represents the mode for discovering resource dependencies.
88
/// </summary>
99
public enum ResourceDependencyDiscoveryMode
1010
{
@@ -20,5 +20,5 @@ public enum ResourceDependencyDiscoveryMode
2020
/// and from environment variables and command-line arguments, but does not recurse
2121
/// into the dependencies of those dependencies.
2222
/// </summary>
23-
DirectOnly
23+
DirectOnly,
2424
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Hosting.ApplicationModel;
5+
6+
/// <summary>
7+
/// Changes how resource dependencies are discovered.
8+
/// </summary>
9+
public sealed class ResourceDependencyDiscoveryOptions
10+
{
11+
/// <summary>
12+
/// Sets the mode for discovering resource dependencies. See <see cref="ResourceDependencyDiscoveryMode"/> for details on the available modes.
13+
///
14+
/// </summary>
15+
public ResourceDependencyDiscoveryMode DiscoveryMode { get; init; }
16+
17+
/// <summary>
18+
/// When true, unresolved values from annotation callbacks will be cached and reused
19+
/// on subsequent evaluations of the same annotation, rather than re-evaluating the callback each time.
20+
/// </summary>
21+
public bool CacheAnnotationCallbackResults { get; init; }
22+
}

0 commit comments

Comments
 (0)