Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0cf7f52
Add a builder pattern for resolving resource configuration
danegsta Nov 27, 2025
80ef95c
Update services available to test
danegsta Dec 1, 2025
2211dc8
Make configuration gatherer implementations internal
danegsta Dec 1, 2025
badbfef
Apply suggestion from @Copilot
danegsta Dec 1, 2025
a1fcffc
Remove unused code
danegsta Dec 1, 2025
a140660
Update src/Aspire.Hosting/ApplicationModel/ResourceServerAuthenticati…
danegsta Dec 1, 2025
10ce666
Update src/Aspire.Hosting/ApplicationModel/ResourceCertificateTrustCo…
danegsta Dec 1, 2025
dc3a8d0
Update src/Aspire.Hosting/ApplicationModel/ResourceServerAuthenticati…
danegsta Dec 1, 2025
1e1a448
Update src/Aspire.Hosting/ApplicationModel/ResourceConfigurationGathe…
danegsta Dec 1, 2025
4e48098
Update doc comment
danegsta Dec 1, 2025
1a487e3
Add missing bracket
danegsta Dec 1, 2025
b90751d
Add test cases for new configuration gatherer implmentations
danegsta Dec 1, 2025
4d4b8c7
Fix missing paramater
danegsta Dec 1, 2025
2bce908
Rename Configuration to ExecutionConfiguration
danegsta Dec 1, 2025
ffdcc62
Merge remote-tracking branch 'upstream/main' into danegsta/referenceT…
danegsta Dec 1, 2025
9fae909
Convert resource evaluation to new bulder pattern
danegsta Dec 2, 2025
7b22da7
Fix failing tests after update
danegsta Dec 2, 2025
6c07355
Check for inner exception
danegsta Dec 2, 2025
16c2035
Switch to returning a tuple
danegsta Dec 3, 2025
c866b9b
Missed one last place to update the tuple
danegsta Dec 3, 2025
4d3c949
Add an extension method to retrieve the builder directly from a resource
danegsta Dec 3, 2025
e14e6ff
Handle empty create file results
danegsta Dec 4, 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
2 changes: 0 additions & 2 deletions src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ async Task<ResolvedValue> EvalExpressionAsync(ReferenceExpression expr, ValuePro
var args = new object?[expr.ValueProviders.Count];
var isSensitive = false;

expr.WasResolved = true;

for (var i = 0; i < expr.ValueProviders.Count; i++)
{
var result = await ResolveInternalAsync(expr.ValueProviders[i], context).ConfigureAwait(false);
Expand Down
31 changes: 31 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/IResourceConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Configuration (arguments and environment variables) to apply to a specific resource.
/// </summary>
public interface IResourceConfiguration
{
/// <summary>
/// Gets the arguments to apply to the resource.
/// </summary>
IReadOnlyList<(string Value, bool IsSensitive)> Arguments { get; }

/// <summary>
/// Gets the environment variables to apply to the resource.
/// </summary>
IReadOnlyDictionary<string, string> EnvironmentVariables { get; }

/// <summary>
/// Gets the metadata associated with the resource configuration.
/// </summary>
IReadOnlySet<IResourceConfigurationMetadata> Metadata { get; }

/// <summary>
/// Gets the exception that occurred while gathering the resource configuration, if any.
/// If multiple exceptions occurred, they are aggregated into an AggregateException.
/// </summary>
Exception? Exception { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Builder for producing the configuration (arguments and environment variables) to apply to a specific resource.
/// </summary>
public interface IResourceConfigurationBuilder
{
/// <summary>
/// Adds a configuration gatherer to the builder.
/// </summary>
/// <param name="gatherer">The configuration gatherer to add.</param>
/// <returns>The current instance of the builder.</returns>
IResourceConfigurationBuilder AddConfigurationGatherer(IResourceConfigurationGatherer gatherer);

/// <summary>
/// Builds the resource configuration.
/// </summary>
/// <param name="executionContext">The execution context.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The resource configuration.</returns>
Task<IResourceConfiguration> BuildAsync(DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Gathers resource configurations (arguments and environment variables) and optionally
/// applies additional metadata to the resource.
/// </summary>
public interface IResourceConfigurationGatherer
{
/// <summary>
/// Gathers the relevant resource configuration.
/// </summary>
/// <param name="context">The initial resource configuration context.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask GatherAsync(IResourceConfigurationGathererContext context, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -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 Microsoft.Extensions.Logging;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Resource configuration gatherer context.
/// </summary>
public interface IResourceConfigurationGathererContext
{
/// <summary>
/// The resource for which configuration is being gathered.
/// </summary>
IResource Resource { get; }

/// <summary>
/// The logger for the resource.
/// </summary>
ILogger ResourceLogger { get; }

/// <summary>
/// The execution context in which the resource is being configured.
/// </summary>
DistributedApplicationExecutionContext ExecutionContext { get; }

/// <summary>
/// Collection of resource command line arguments.
/// </summary>
List<object> Arguments { get; }

/// <summary>
/// Collection of resource environment variables.
/// </summary>
Dictionary<string, object> EnvironmentVariables { get; }

/// <summary>
/// Adds metadata associated with the resource configuration.
/// </summary>
/// <param name="metadata">The metadata to add.</param>
void AddMetadata(IResourceConfigurationMetadata metadata);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Metadata associated with a resource configuration.
/// </summary>
public interface IResourceConfigurationMetadata
{
}
7 changes: 0 additions & 7 deletions src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,13 @@ private ReferenceExpression(string format, IValueProvider[] valueProviders, stri
public string ValueExpression =>
string.Format(CultureInfo.InvariantCulture, Format, _manifestExpressions);

/// <summary>
/// Indicates whether this expression was ever referenced to get its value.
/// </summary>
internal bool WasResolved { get; set; }

/// <summary>
/// Gets the value of the expression. The final string value after evaluating the format string and its parameters.
/// </summary>
/// <param name="context">A context for resolving the value.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param>
public async ValueTask<string?> GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken)
{
WasResolved = true;

// NOTE: any logical changes to this method should also be made to ExpressionResolver.EvalExpressionAsync
if (Format.Length == 0)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Gathers command line arguments for resources.
/// </summary>
internal class ResourceArgumentsConfigurationGatherer : IResourceConfigurationGatherer
{
/// <inheritdoc/>
public async ValueTask GatherAsync(IResourceConfigurationGathererContext context, CancellationToken cancellationToken = default)
{
if (context.Resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var callbacks))
{
var callbackContext = new CommandLineArgsCallbackContext(context.Arguments, context.Resource, cancellationToken)
{
Logger = context.ResourceLogger,
ExecutionContext = context.ExecutionContext
};

foreach (var callback in callbacks)
{
await callback.Callback(callbackContext).ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIRECERTIFICATES001

using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Gathers certificate trust configuration for resources that require it.
/// </summary>
internal class ResourceCertificateTrustConfigurationGatherer : IResourceConfigurationGatherer
{
private readonly Func<CertificateTrustScope, CertificateTrustConfigurationContext> _configContextFactory;

/// <summary>
/// Initializes a new instance of <see cref="ResourceCertificateTrustConfigurationGatherer"/>.
/// </summary>
/// <param name="configContextFactory">A factory for configuring certificate trust configuration properties.</param>
public ResourceCertificateTrustConfigurationGatherer(Func<CertificateTrustScope, CertificateTrustConfigurationContext> configContextFactory)
{
_configContextFactory = configContextFactory;
}

/// <inheritdoc/>
public async ValueTask GatherAsync(IResourceConfigurationGathererContext context, CancellationToken cancellationToken = default)
{
var developerCertificateService = context.ExecutionContext.ServiceProvider.GetRequiredService<IDeveloperCertificateService>();
var trustDevCert = developerCertificateService.TrustCertificate;

// Add additional certificate trust configuration metadata
var metadata = new CertificateTrustConfigurationMetadata();
context.AddMetadata(metadata);

metadata.Scope = CertificateTrustScope.Append;
var certificates = new X509Certificate2Collection();
if (context.Resource.TryGetLastAnnotation<CertificateAuthorityCollectionAnnotation>(out var caAnnotation))
{
foreach (var certCollection in caAnnotation.CertificateAuthorityCollections)
{
certificates.AddRange(certCollection.Certificates);
}

trustDevCert = caAnnotation.TrustDeveloperCertificates.GetValueOrDefault(trustDevCert);
metadata.Scope = caAnnotation.Scope.GetValueOrDefault(metadata.Scope);
}

if (metadata.Scope == CertificateTrustScope.None)
{
// No certificate trust configuration to apply
return;
}

if (metadata.Scope == CertificateTrustScope.System)
{
// Read the system root certificates and add them to the collection
certificates.AddRootCertificates();
}

if (context.ExecutionContext.IsRunMode && trustDevCert)
{
foreach (var cert in developerCertificateService.Certificates)
{
certificates.Add(cert);
}
}

metadata.Certificates.AddRange(certificates);

if (!metadata.Certificates.Any())
{
// No certificates to configure
context.ResourceLogger.LogInformation("No custom certificate authorities to configure for '{ResourceName}'. Default certificate authority trust behavior will be used.", context.Resource.Name);
return;
}

var configurationContext = _configContextFactory(metadata.Scope);

// Apply default OpenSSL environment configuration for certificate trust
context.EnvironmentVariables["SSL_CERT_DIR"] = configurationContext.CertificateDirectoriesPath;

if (metadata.Scope != CertificateTrustScope.Append)
{
context.EnvironmentVariables["SSL_CERT_FILE"] = configurationContext.CertificateBundlePath;
}

var callbackContext = new CertificateTrustConfigurationCallbackAnnotationContext
{
ExecutionContext = context.ExecutionContext,
Resource = context.Resource,
Scope = metadata.Scope,
CertificateBundlePath = configurationContext.CertificateBundlePath,
CertificateDirectoriesPath = configurationContext.CertificateDirectoriesPath,
Arguments = context.Arguments,
EnvironmentVariables = context.EnvironmentVariables,
CancellationToken = cancellationToken,
};

if (context.Resource.TryGetAnnotationsOfType<CertificateTrustConfigurationCallbackAnnotation>(out var callbacks))
{
foreach (var callback in callbacks)
{
await callback.Callback(callbackContext).ConfigureAwait(false);
}
}

if (metadata.Scope == CertificateTrustScope.System)
{
context.ResourceLogger.LogInformation("Resource '{ResourceName}' has a certificate trust scope of '{Scope}'. Automatically including system root certificates in the trusted configuration.", context.Resource.Name, Enum.GetName(metadata.Scope));
}

}
}

/// <summary>
/// Metadata about the resource certificate trust configuration.
/// </summary>
public class CertificateTrustConfigurationMetadata : IResourceConfigurationMetadata
{
/// <summary>
/// The certificate trust scope for the resource.
/// </summary>
public CertificateTrustScope Scope { get; internal set; }

/// <summary>
/// The collection of certificates to trust.
/// </summary>
public X509Certificate2Collection Certificates { get; } = new();
}

/// <summary>
/// Context for configuring certificate trust configuration properties.
/// </summary>
public class CertificateTrustConfigurationContext
{
/// <summary>
/// The path to the certificate bundle file in the resource context (e.g., container filesystem).
/// </summary>
public required ReferenceExpression CertificateBundlePath { get; init; }

/// <summary>
/// The path(s) to the certificate directories in the resource context (e.g., container filesystem).
/// </summary>
public required ReferenceExpression CertificateDirectoriesPath { get; init; }
}
22 changes: 22 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ResourceConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents the configuration (arguments and environment variables) to apply to a specific resource.
/// </summary>
internal class ResourceConfiguration : IResourceConfiguration
{
/// <inheritdoc/>
public required IReadOnlyList<(string Value, bool IsSensitive)> Arguments { get; init; }

/// <inheritdoc/>
public required IReadOnlyDictionary<string, string> EnvironmentVariables { get; init; }

/// <inheritdoc/>
public required IReadOnlySet<IResourceConfigurationMetadata> Metadata { get; init; }

/// <inheritdoc/>
public required Exception? Exception { get; init; }
}
Loading