Skip to content

Commit

Permalink
Use IConfiguration (#2011)
Browse files Browse the repository at this point in the history
* Use IConfiguration

This change removes the `IEnvironmentVariables` abstraction, replacing it and usages with `IConfiguration` from the framework.

The built-in `IConfiguration` system is more flexible, allowing configuration values to come from various other sources.

* Remove MockConfiguration class

* Use IConfiguration in ApplicationExecutor
  • Loading branch information
drewnoakes authored Jan 31, 2024
1 parent 762ec09 commit 73004d9
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 223 deletions.
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Aspire.Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
<Link>Protos\resource_service.proto</Link>
</Protobuf>
<Compile Include="$(SharedDir)ChannelExtensions.cs" Link="Extensions\ChannelExtensions.cs" />
<Compile Include="$(SharedDir)IEnvironmentVariables.cs" Link="Utils\IEnvironmentVariables.cs" />
<Compile Include="$(SharedDir)IConfigurationExtensions.cs" Link="Utils\IConfigurationExtensions.cs" />
<Compile Include="$(SharedDir)KnownResourceNames.cs" Link="Utils\KnownResourceNames.cs" />
<Compile Include="$(SharedDir)KnownFormats.cs" Link="Utils\KnownFormats.cs" />
<Compile Include="$(SharedDir)StringComparers.cs" Link="Utils\StringComparers.cs" />
Expand Down
8 changes: 2 additions & 6 deletions src/Aspire.Dashboard/DashboardWebApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,9 @@ public DashboardWebApplication()
builder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.None);
builder.Logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel", LogLevel.Error);

var environmentVariables = new EnvironmentVariables();
var dashboardUris = builder.Configuration.GetUris(DashboardUrlVariableName, new(DashboardUrlDefaultValue));

builder.Services.AddSingleton<IEnvironmentVariables>(environmentVariables);

var dashboardUris = environmentVariables.GetUris(DashboardUrlVariableName, new(DashboardUrlDefaultValue));

var otlpUris = environmentVariables.GetUris(DashboardOtlpUrlVariableName, new(DashboardOtlpUrlDefaultValue));
var otlpUris = builder.Configuration.GetUris(DashboardOtlpUrlVariableName, new(DashboardOtlpUrlDefaultValue));

if (otlpUris.Length > 1)
{
Expand Down
10 changes: 5 additions & 5 deletions src/Aspire.Dashboard/Model/DashboardClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal sealed class DashboardClient : IDashboardClient
private readonly object _lock = new();

private readonly ILoggerFactory _loggerFactory;
private readonly IEnvironmentVariables _environmentVariables;
private readonly IConfiguration _configuration;
private readonly ILogger<DashboardClient> _logger;

private ImmutableHashSet<Channel<ResourceViewModelChange>> _outgoingChannels = [];
Expand All @@ -56,14 +56,14 @@ internal sealed class DashboardClient : IDashboardClient

private Task? _connection;

public DashboardClient(ILoggerFactory loggerFactory, IEnvironmentVariables environmentVariables)
public DashboardClient(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_loggerFactory = loggerFactory;
_environmentVariables = environmentVariables;
_configuration = configuration;

_logger = loggerFactory.CreateLogger<DashboardClient>();

var address = environmentVariables.GetUri(ResourceServiceUrlVariableName);
var address = configuration.GetUri(ResourceServiceUrlVariableName);

if (address is null)
{
Expand Down Expand Up @@ -307,7 +307,7 @@ Task IDashboardClient.WhenConnected
string IDashboardClient.ApplicationName
{
get => _applicationName
?? _environmentVariables.GetString("DOTNET_DASHBOARD_APPLICATION_NAME")
?? _configuration["DOTNET_DASHBOARD_APPLICATION_NAME"]
?? "Aspire";
}

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Aspire.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<Compile Include="$(SharedDir)Model\KnownProperties.cs" Link="Dashboard\KnownProperties.cs" />
<Compile Include="$(SharedDir)Model\KnownResourceTypes.cs" Link="Dashboard\KnownResourceTypes.cs" />
<Compile Include="$(SharedDir)ChannelExtensions.cs" Link="Extensions\ChannelExtensions.cs" />
<Compile Include="$(SharedDir)IEnvironmentVariables.cs" Link="Utils\IEnvironmentVariables.cs" />
<Compile Include="$(SharedDir)IConfigurationExtensions.cs" Link="Utils\IConfigurationExtensions.cs" />
<Compile Include="$(SharedDir)KnownResourceNames.cs" Link="Utils\KnownResourceNames.cs" />
<Compile Include="$(SharedDir)StringComparers.cs" Link="Utils\StringComparers.cs" />
<Compile Include="$(SharedDir)KnownFormats.cs" Link="Utils\KnownFormats.cs" />
Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Hosting/Dashboard/DashboardServiceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting.Dashboard;
Expand All @@ -21,13 +22,13 @@ internal sealed class DashboardServiceData : IAsyncDisposable
public DashboardServiceData(
DistributedApplicationModel applicationModel,
KubernetesService kubernetesService,
IEnvironmentVariables environmentVariables,
IConfiguration configuration,
ILoggerFactory loggerFactory)
{
_resourcePublisher = new ResourcePublisher(_cts.Token);
_consoleLogPublisher = new ConsoleLogPublisher(_resourcePublisher);

_ = new DcpDataSource(kubernetesService, applicationModel, environmentVariables, loggerFactory, _resourcePublisher.IntegrateAsync, _cts.Token);
_ = new DcpDataSource(kubernetesService, applicationModel, configuration, loggerFactory, _resourcePublisher.IntegrateAsync, _cts.Token);
}

public async ValueTask DisposeAsync()
Expand Down
9 changes: 5 additions & 4 deletions src/Aspire.Hosting/Dashboard/DashboardServiceHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Internal;
Expand Down Expand Up @@ -49,7 +50,7 @@ public DashboardServiceHost(
DistributedApplicationOptions options,
DistributedApplicationModel applicationModel,
KubernetesService kubernetesService,
IEnvironmentVariables environmentVariables,
IConfiguration configuration,
IOptions<PublishingOptions> publishingOptions,
ILoggerFactory loggerFactory,
IConfigureOptions<LoggerFilterOptions> loggerOptions)
Expand All @@ -68,8 +69,8 @@ public DashboardServiceHost(
{
var builder = WebApplication.CreateBuilder();

// Environment
builder.Services.AddSingleton<IEnvironmentVariables, EnvironmentVariables>();
// Configuration
builder.Services.AddSingleton(configuration);

// Logging
builder.Services.AddSingleton(loggerFactory);
Expand Down Expand Up @@ -98,7 +99,7 @@ public DashboardServiceHost(
void ConfigureKestrel(KestrelServerOptions kestrelOptions)
{
// Inspect environment for the address to listen on.
var uri = environmentVariables.GetUri(ResourceServiceUrlVariableName);
var uri = configuration.GetUri(ResourceServiceUrlVariableName);

string? scheme;

Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Hosting/Dashboard/DcpDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Dcp.Model;
using k8s;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting.Dashboard;
Expand Down Expand Up @@ -35,7 +36,7 @@ internal sealed class DcpDataSource
public DcpDataSource(
KubernetesService kubernetesService,
DistributedApplicationModel applicationModel,
IEnvironmentVariables environmentVariables,
IConfiguration configuration,
ILoggerFactory loggerFactory,
Func<ResourceSnapshot, ResourceSnapshotChangeType, ValueTask> onResourceChanged,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -96,7 +97,7 @@ bool IsFilteredResource<T>(T resource) where T : CustomResource
// We filter out any resources that start with aspire-dashboard (there are services as well as executables).
if (resource.Metadata.Name.StartsWith(KnownResourceNames.AspireDashboard, StringComparisons.ResourceName))
{
return environmentVariables.GetBool("DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES") is not true;
return configuration.GetBool("DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES") is not true;
}

return false;
Expand Down
23 changes: 12 additions & 11 deletions src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Utils;
using k8s;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -55,7 +56,7 @@ internal sealed class ApplicationExecutor(ILogger<ApplicationExecutor> logger,
DistributedApplicationOptions distributedApplicationOptions,
KubernetesService kubernetesService,
IEnumerable<IDistributedApplicationLifecycleHook> lifecycleHooks,
IEnvironmentVariables environmentVariables,
IConfiguration configuration,
IOptions<DcpOptions> options,
DashboardServiceHost dashboardHost)
{
Expand Down Expand Up @@ -137,12 +138,12 @@ private async Task ConfigureAspireDashboardResource(IResource dashboardResource,

dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(context =>
{
if (Environment.GetEnvironmentVariable("ASPNETCORE_URLS") is not { } appHostApplicationUrl)
if (configuration["ASPNETCORE_URLS"] is not { } appHostApplicationUrl)
{
throw new DistributedApplicationException("Failed to configure dashboard resource because ASPNETCORE_URLS environment variable was not set.");
}

if (Environment.GetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL") is not { } otlpEndpointUrl)
if (configuration["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] is not { } otlpEndpointUrl)
{
throw new DistributedApplicationException("Failed to configure dashboard resource because DOTNET_DASHBOARD_OTLP_ENDPOINT_URL environment variable was not set.");
}
Expand Down Expand Up @@ -202,9 +203,9 @@ private async Task StartDashboardAsDcpExecutableAsync(CancellationToken cancella
// Matches DashboardWebApplication.DashboardUrlDefaultValue
const string defaultDashboardUrl = "http://localhost:18888";

var otlpEndpointUrl = environmentVariables.GetString("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL");
var dashboardUrls = environmentVariables.GetString("ASPNETCORE_URLS") ?? defaultDashboardUrl;
var aspnetcoreEnvironment = environmentVariables.GetString("ASPNETCORE_ENVIRONMENT");
var otlpEndpointUrl = configuration["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"];
var dashboardUrls = configuration["ASPNETCORE_URLS"] ?? defaultDashboardUrl;
var aspnetcoreEnvironment = configuration["ASPNETCORE_ENVIRONMENT"];

dashboardExecutableSpec.Env =
[
Expand Down Expand Up @@ -239,11 +240,11 @@ private async Task StartDashboardAsDcpExecutableAsync(CancellationToken cancella
await CheckDashboardAvailabilityAsync(dashboardUrls, cancellationToken).ConfigureAwait(false);
}

private static TimeSpan DashboardAvailabilityTimeoutDuration
private TimeSpan DashboardAvailabilityTimeoutDuration
{
get
{
if (Environment.GetEnvironmentVariable("DOTNET_ASPIRE_DASHBOARD_TIMEOUT_SECONDS") is { } timeoutString && int.TryParse(timeoutString, out var timeoutInSeconds))
if (configuration["DOTNET_ASPIRE_DASHBOARD_TIMEOUT_SECONDS"] is { } timeoutString && int.TryParse(timeoutString, out var timeoutInSeconds))
{
return TimeSpan.FromSeconds(timeoutInSeconds);
}
Expand Down Expand Up @@ -492,7 +493,7 @@ private void PrepareProjectExecutables()
annotationHolder.Annotate(Executable.CSharpProjectPathAnnotation, projectMetadata.ProjectPath);
annotationHolder.Annotate(Executable.OtelServiceNameAnnotation, ers.Metadata.Name);

if (!string.IsNullOrEmpty(environmentVariables.GetString(DebugSessionPortVar)))
if (!string.IsNullOrEmpty(configuration[DebugSessionPortVar]))
{
exeSpec.ExecutionType = ExecutionType.IDE;
if (project.TryGetLastAnnotation<LaunchProfileAnnotation>(out var lpa))
Expand All @@ -503,7 +504,7 @@ private void PrepareProjectExecutables()
else
{
exeSpec.ExecutionType = ExecutionType.Process;
if (environmentVariables.GetBool("DOTNET_WATCH") is true)
if (configuration.GetBool("DOTNET_WATCH") is true)
{
exeSpec.Args = [
"run",
Expand Down Expand Up @@ -657,7 +658,7 @@ private async Task CreateExecutablesAsync(IEnumerable<AppResource> executableRes
{
// We just check the HTTP endpoint because this will prove that the
// dashboard is listening and is ready to process requests.
if (Environment.GetEnvironmentVariable("ASPNETCORE_URLS") is not { } dashboardUrls)
if (configuration["ASPNETCORE_URLS"] is not { } dashboardUrls)
{
throw new DistributedApplicationException("Cannot check dashboard availability since ASPNETCORE_URLS environment variable not set.");
}
Expand Down
1 change: 0 additions & 1 deletion src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Services.AddHostedService<DistributedApplicationLifecycle>();
_innerBuilder.Services.AddHostedService<DistributedApplicationRunner>();
_innerBuilder.Services.AddSingleton(options);
_innerBuilder.Services.AddSingleton<IEnvironmentVariables, EnvironmentVariables>();

// Dashboard
_innerBuilder.Services.AddSingleton<DashboardServiceHost>();
Expand Down
121 changes: 121 additions & 0 deletions src/Shared/IConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;

namespace Aspire;

internal static class IConfigurationExtensions
{
/// <summary>
/// Gets the named configuration value as a boolean.
/// </summary>
/// <remarks>
/// Parses <c>true</c> and <c>false</c>, along with integer values (where non-zero is <see langword="true"/>).
/// </remarks>
/// <param name="configuration">The <see cref="IConfiguration"/> this method extends.</param>
/// <param name="key">The configuration key.</param>
/// <returns>The parsed value, or <see langword="null"/> if no value exists or it couldn't be parsed.</returns>
public static bool? GetBool(this IConfiguration configuration, string key)
{
var value = configuration[key];

if (value is null or [])
{
return null;
}
else if (bool.TryParse(value, out var b))
{
return b;
}
else if (int.TryParse(value, out var i))
{
return i != 0;
}

return null;
}

/// <summary>
/// Gets the named configuration value as a boolean.
/// </summary>
/// <remarks>
/// Parses <c>true</c> and <c>false</c>, along with <c>1</c> and <c>0</c>.
/// </remarks>
/// <param name="configuration">The <see cref="IConfiguration"/> this method extends.</param>
/// <param name="key">The configuration key.</param>
/// <param name="defaultValue">A default value, for when the configuration value is unspecified or white space.</param>
/// <returns></returns>
public static bool GetBool(this IConfiguration configuration, string key, bool defaultValue)
{
return configuration.GetBool(key) ?? defaultValue;
}

/// <summary>
/// Parses a configuration value into a <see cref="Uri"/> object.
/// </summary>
/// <param name="configuration">The <see cref="IConfiguration"/> this method extends.</param>
/// <param name="key">The configuration key.</param>
/// <param name="defaultValue">A default value, for when the configuration value is unspecified or white space. May be <see langword="null"/>.</param>
/// <returns>The parsed value, or the default value if specified and parsing failed. Returns <see langword="null"/> if <paramref name="defaultValue"/> is <see langword="null"/> and parsing failed.</returns>
/// <exception cref="InvalidOperationException">The configuration value could not be accessed, or contained incorrectly formatted data.</exception>
[return: NotNullIfNotNull(nameof(defaultValue))]
public static Uri? GetUri(this IConfiguration configuration, string key, Uri? defaultValue = null)
{
try
{
var uri = configuration[key];

if (string.IsNullOrWhiteSpace(uri))
{
return defaultValue;
}
else
{
return new Uri(uri, UriKind.Absolute);
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error parsing URIs from configuration value '{key}'.", ex);
}
}

/// <summary>
/// Parses a configuration value's semicolon-delimited value into an array of <see cref="Uri"/> objects.
/// </summary>
/// <param name="configuration">The <see cref="IConfiguration"/> this method extends.</param>
/// <param name="key">The configuration key.</param>
/// <param name="defaultValue">A default value, for when the configuration value is unspecified or white space. May be <see langword="null"/>.</param>
/// <returns>The parsed values, or the default value if specified and parsing failed. Returns <see langword="null"/> if <paramref name="defaultValue"/> is <see langword="null"/> and parsing failed.</returns>
/// <exception cref="InvalidOperationException">The configuration value could not be accessed, or contained incorrectly formatted data.</exception>
[return: NotNullIfNotNull(nameof(defaultValue))]
public static Uri[]? GetUris(this IConfiguration configuration, string key, Uri? defaultValue = null)
{
try
{
var uris = configuration[key];

if (string.IsNullOrWhiteSpace(uris))
{
return defaultValue switch
{
not null => [defaultValue],
null => null
};
}
else
{
return uris
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(url => new Uri(url, UriKind.Absolute))
.ToArray();
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error parsing URIs from configuration value '{key}'.", ex);
}
}
}
Loading

0 comments on commit 73004d9

Please sign in to comment.