dotnet / dotnet/runtime

[API Proposal]: In Microsoft.Extensions.Configuration.EnvironmentVariables add property: EnvironmentVariableTarget to allow loading from the Registry [Machine/User]

Open
#123,219 2 comments 0 reactions 1 assignee Claimed by @mrek-msft View on GitHub
api-suggestion area-Extensions-Configuration
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Background and motivation

Microsoft.Extensions.Configuration.AddEnvironmentVariables loads environment variables using System.Environment.GetEnvironmentVariables that uses the processes current context to retrieve the environment variables and properties. This proposal is to allow reading environment variables directly from the Windows Registry, allowing you to access Machine or User-level environment variables that may not be loaded into the current process.

When deploying an ASP.Net Core application to a Windows IIS webserver, the parent process must be restarted for builder.Configuration to see any newly added env. variables or changed properties. This requires interrupting more than just the specific application pool to reload the parent process to refresh the list of environment variables and properties that the application pool's w3wp.exe process sees. If the Webserver has multiple sites and application pools, they all will have to be interrupted. More background: https://andrewlock.net/setting-environment-variables-in-iis-and-avoiding-app-pool-restarts/.

Having an option to read current environment variables and properties directly from the registry by calling the extension method: System.Environment.GetEnvironmentVariables(EnvironmentvariableTarget.Machine) would eliminate this deployment headache.

### API Proposal

https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Configuration.EnvironmentVariables/src/EnvironmentVariablesConfigurationProvider.cs

In: `EnvironmentVariablesExtension.cs`
```csharp
///
/// Extension methods for registering with .
///
public static class EnvironmentVariablesExtensions
{
///
/// Adds environment variables from the selected Target to the configuration builder.
///
/// The to add to.
/// The registry target (Process, Machine or User). Defaults to Process.
/// The .
public static IConfigurationBuilder AddEnvironmentVariables(
this IConfigurationBuilder builder,
EnvironmentVariableTarget target = EnvironmentVariableTarget.Process)
{
return builder.Add(new EnvironmentVariablesConfigurationSource
{
Target = target
});
}

///
/// Adds environment variables from the selected Target to the configuration builder with a prefix filter.
///
/// The to add to.
/// The prefix used to filter environment variables. Only variables starting with this prefix will be included.
/// The registry target (Process, Machine or User). Defaults to Process.
/// The .
public static IConfigurationBuilder AddEnvironmentVariables(
this IConfigurationBuilder builder,
string prefix,
EnvironmentVariableTarget target = EnvironmentVariableTarget.Process)
{
return builder.Add(new EnvironmentVariablesConfigurationSource
{
Target = target,
Prefix = prefix
});
}

///
/// Adds environment variables from the Windows Registry to the configuration builder with custom configuration.
///
/// The to add to.
/// Configures the source.
/// The .
public static IConfigurationBuilder AddEnvironmentVariables(
this IConfigurationBuilder builder,
Action configureSource)
{
var source = new EnvironmentVariablesConfigurationSource();
configureSource(source);
return builder.Add(source);
}
}
```

In: `EnvironmentVariablesConfigurationSource.cs`
```csharp
public class EnvironmentVariablesConfigurationSource : IConfigurationSource
{
///
/// Gets or sets the target scope for environment variables.
///
public EnvironmentVariableTarget Target { get; set; } = EnvironmentVariableTarget.Process;

///
/// Gets or sets a prefix used to filter environment variables.
///
public string? Prefix { get; set; }

///
/// Builds the for this source.
///
/// The .
/// A
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new EnvironmentVariablesConfigurationProvider(Target, Prefix);
}
}
```

in: `EnvironmentVariablesConfigurationProvider.cs`
```csharp
public class RegistryEnvironmentVariablesConfigurationProvider : ConfigurationProvider
{
// Connection string prefixes for various services. These prefixes are used to identify connection strings in environment variables.
// az webapp config connection-string set: https://learn.microsoft.com/en-us/cli/azure/webapp/config/connection-string?view=azure-cli-latest#az-webapp-config-connection-string-set
// Environment variables and app settings in Azure App Service: https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings?tabs=kudu%2Cdotnet#variable-prefixes
private const string MySqlServerPrefix = "MYSQLCONNSTR_";
private const string SqlAzureServerPrefix = "SQLAZURECONNSTR_";
private const string SqlServerPrefix = "SQLCONNSTR_";
private const string CustomConnectionStringPrefix = "CUSTOMCONNSTR_";
private const string PostgreSqlServerPrefix = "POSTGRESQLCONNSTR_";
private const string ApiHubPrefix = "APIHUBCONNSTR_";
private const string DocDbPrefix = "DOCDBCONNSTR_";
private const string EventHubPrefix = "EVENTHUBCONNSTR_";
private const string NotificationHubPrefix = "NOTIFICATIONHUBCONNSTR_";
private const string RedisCachePrefix = "REDISCACHECONNSTR_";
private const string ServiceBusPrefix = "SERVICEBUSCONNSTR_";

private readonly EnvironmentVariableTarget _environmentVariableTarget;
private readonly string? _prefix;
private readonly string _normalizedPrefix;

///
/// Initializes a new instance of .
///
/// The registry environmentVariableTarget (Machine or User).
/// Optional prefix to filter environment variables.
public EnvironmentVariablesConfigurationProvider(EnvironmentVariableTarget environmentVariableTarget = EnvironmentVariableTarget=Process, string? prefix = null)
{
_environmentVariableTarget = environmentVariableTarget;
_prefix = prefix ?? string.Empty;
_normalizedPrefix = Normalize(_prefix);
}

///
/// Generates a string representing this provider key and relevant details.
///
/// The configuration key.
public override string ToString()
{
string s = GetType().Name;
if (!string.IsNullOrEmpty(_prefix))
{
s += $" Prefix: '{_prefix}'";
}
return s;
}

///
/// Loads environment variables from the Target
///
public override void Load()
{
var data = new Dictionary(StringComparer.OrdinalIgnoreCase);

try
{
var environmentVariables = Environment.GetEnvironmentVariables(_environmentVariableTarget);

foreach (DictionaryEntry entry in environmentVariables)
{
string key = (string)entry.Key;
string? value = (string?)entry.Value;

if (key.StartsWith(MySqlServerPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, MySqlServerPrefix, "MySql.Data.MySqlClient", key, value);
}
else if (key.StartsWith(SqlAzureServerPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, SqlAzureServerPrefix, "System.Data.SqlClient", key, value);
}
else if (key.StartsWith(SqlServerPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, SqlServerPrefix, "System.Data.SqlClient", key, value);
}
else if (key.StartsWith(PostgreSqlServerPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, PostgreSqlServerPrefix, "Npgsql", key, value);
}
else if (key.StartsWith(ApiHubPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, ApiHubPrefix, null, key, value);
}
else if (key.StartsWith(DocDbPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, DocDbPrefix, null, key, value);
}
else if (key.StartsWith(EventHubPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, EventHubPrefix, null, key, value);
}
else if (key.StartsWith(NotificationHubPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, NotificationHubPrefix, null, key, value);
}
else if (key.StartsWith(RedisCachePrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, RedisCachePrefix, null, key, value);
}
else if (key.StartsWith(ServiceBusPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, ServiceBusPrefix, null, key, value);
}
else if (key.StartsWith(CustomConnectionStringPrefix, StringComparison.OrdinalIgnoreCase))
{
HandleMatchedConnectionStringPrefix(data, CustomConnectionStringPrefix, null, key, value);
}
else
{
AddIfNormalizedKeyMatchesPrefix(data, Normalize(key), value);
}
}
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Failed to read environment variables from registry environmentVariableTarget '{_environmentVariableTarget}'.", ex);
}

Data = data;
}

private void HandleMatchedConnectionStringPrefix(Dictionary data, string connectionStringPrefix, string? provider, string fullKey, string? value)
{
string normalizedKeyWithoutConnectionStringPrefix = Normalize(fullKey.Substring(connectionStringPrefix.Length));

// Add the key-value pair for connection string, and optionally provider key
AddIfNormalizedKeyMatchesPrefix(data, $"ConnectionStrings:{normalizedKeyWithoutConnectionStringPrefix}", value);
if (provider != null)
{
AddIfNormalizedKeyMatchesPrefix(data, $"ConnectionStrings:{normalizedKeyWithoutConnectionStringPrefix}_ProviderName", provider);
}
}

private void AddIfNormalizedKeyMatchesPrefix(Dictionary data, string normalizedKey, string? value)
{
if (normalizedKey.StartsWith(_normalizedPrefix, StringComparison.OrdinalIgnoreCase))
{
data[normalizedKey.Substring(_normalizedPrefix.Length)] = value;
}
}

private static string Normalize(string key) => key.Replace("__", ConfigurationPath.KeyDelimiter);
}
```

### API Usage

In: `Program.cs`

```csharp

// Change the target from Process to Machine (Registry), when binding EnvironmentVariables and properties to Application Settings / Configuration.
var source = builder.Configuration.Sources.OfType().FirstOrDefault();
source.Target = EnvironmentVariableTarget.Machine;

var appSettings = builder.Configuration.Get();
```

### Alternative Designs

An additional Configuration provider could be created to allow users to optional call buildr.Configuration.AddRegistryEnvironmentVariables(EnvironmentVariableTarget.Machine)

### Risks

This feature is limited to the Windows platform as it has a Registry that can be accessed Win32 calls made in System.Environment.GetEnvironmentVariables([EnvironmentVariableTarget])

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.