|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Aspire.Hosting.Utils;
namespace Aspire.Hosting.ApplicationModel;
internal class ExpressionResolver(CancellationToken cancellationToken)
{
async Task<string?> ResolveInContainerContextAsync(EndpointReference endpointReference, EndpointProperty property, ValueProviderContext context)
{
// We need to use the root resource, e.g. AzureStorageResource instead of AzureBlobResource
// Otherwise, we get the wrong values for IsContainer and Name
var target = endpointReference.Resource.GetRootResource();
return (property, target.IsContainer()) switch
{
// If Container -> Container, we use container name as host, and target port as port
// This assumes both containers are on the same container network.
// Different networks will require addtional routing/tunneling that we do not support today.
(EndpointProperty.Host or EndpointProperty.IPV4Host, true) => target.Name,
(EndpointProperty.Port, true) => await endpointReference.Property(EndpointProperty.TargetPort).GetValueAsync(context, cancellationToken).ConfigureAwait(false),
(EndpointProperty.Url, _) => string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}",
endpointReference.Scheme,
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Host, context).ConfigureAwait(false),
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Port, context).ConfigureAwait(false)),
(EndpointProperty.HostAndPort, _) => string.Format(CultureInfo.InvariantCulture, "{0}:{1}",
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Host, context).ConfigureAwait(false),
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Port, context).ConfigureAwait(false)),
_ => await endpointReference.Property(property).GetValueAsync(context, cancellationToken).ConfigureAwait(false)
};
}
async Task<ResolvedValue> EvalExpressionAsync(ReferenceExpression expr, ValueProviderContext context)
{
// This logic is similar to ReferenceExpression.GetValueAsync, except that we recurse on
// our own resolver method
var args = new object?[expr.ValueProviders.Count];
var isSensitive = false;
for (var i = 0; i < expr.ValueProviders.Count; i++)
{
var result = await ResolveInternalAsync(expr.ValueProviders[i], context).ConfigureAwait(false);
args[i] = result?.Value;
// Apply string format if needed
if (expr.StringFormats[i] is { } stringFormat && args[i] is string s)
{
args[i] = FormattingHelpers.FormatValue(s, stringFormat);
}
if (result?.IsSensitive is true)
{
isSensitive = true;
}
}
// Identically to ReferenceExpression.GetValueAsync, we return null if the format is empty
var value = expr.Format.Length == 0 ? null : string.Format(CultureInfo.InvariantCulture, expr.Format, args);
return new ResolvedValue(value, isSensitive);
}
async Task<ResolvedValue> EvalValueProvider(IValueProvider vp, ValueProviderContext context)
{
var value = await vp.GetValueAsync(context, cancellationToken).ConfigureAwait(false);
if (vp is ParameterResource pr)
{
return new ResolvedValue(value, pr.Secret);
}
return new ResolvedValue(value, false);
}
async Task<ResolvedValue> ResolveConnectionStringReferenceAsync(ConnectionStringReference cs, ValueProviderContext context)
{
// We are substituting our own logic for ConnectionStringReference's GetValueAsync.
// However, ConnectionStringReference#GetValueAsync will throw if the connection string is not optional but is not present.
// so we need to do the same here.
var value = await ResolveInternalAsync(cs.Resource.ConnectionStringExpression, context).ConfigureAwait(false);
// Throw if the connection string is required but not present
if (string.IsNullOrEmpty(value.Value) && !cs.Optional)
{
cs.ThrowConnectionStringUnavailableException();
}
return value;
}
/// <summary>
/// Resolve an expression. When it is being used from inside a container, endpoints may be evaluated (either in a container-to-container or container-to-exe communication).
/// </summary>
async ValueTask<ResolvedValue> ResolveInternalAsync(object? value, ValueProviderContext context)
{
var networkContext = context.GetNetworkIdentifier();
return value switch
{
ConnectionStringReference cs => await ResolveConnectionStringReferenceAsync(cs, context).ConfigureAwait(false),
IResourceWithConnectionString cs and not ConnectionStringParameterResource => await ResolveInternalAsync(cs.ConnectionStringExpression, context).ConfigureAwait(false),
ReferenceExpression ex => await EvalExpressionAsync(ex, context).ConfigureAwait(false),
EndpointReference er when networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(er, EndpointProperty.Url, context).ConfigureAwait(false), false),
EndpointReferenceExpression ep when networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(ep.Endpoint, ep.Property, context).ConfigureAwait(false), false),
IValueProvider vp => await EvalValueProvider(vp, context).ConfigureAwait(false),
_ => throw new NotImplementedException()
};
}
internal static async ValueTask<ResolvedValue> ResolveAsync(IValueProvider valueProvider, ValueProviderContext context, CancellationToken cancellationToken)
{
var resolver = new ExpressionResolver(cancellationToken);
return await resolver.ResolveInternalAsync(valueProvider, context).ConfigureAwait(false);
}
}
internal record ResolvedValue(string? Value, bool IsSensitive);
|