|
// 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;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting.Pipelines.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
namespace Aspire.Hosting.UserSecrets;
/// <summary>
/// Factory for creating and caching <see cref="IUserSecretsManager"/> instances.
/// </summary>
/// <remarks>
/// Uses a lock to ensure thread-safe creation and a dictionary to cache instances by normalized file path.
/// </remarks>
internal sealed class UserSecretsManagerFactory
{
// Singleton instance
public static readonly UserSecretsManagerFactory Instance = new();
// Dictionary to cache instances by file path
private readonly Dictionary<string, IUserSecretsManager> _managerCache = new();
private readonly object _lock = new();
internal UserSecretsManagerFactory()
{
}
/// <summary>
/// Gets or creates a user secrets manager for the specified file path.
/// </summary>
public IUserSecretsManager GetOrCreate(string filePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
var normalizedPath = Path.GetFullPath(filePath);
lock (_lock)
{
if (!_managerCache.TryGetValue(normalizedPath, out var manager))
{
manager = new UserSecretsManager(normalizedPath);
_managerCache[normalizedPath] = manager;
}
return manager;
}
}
/// <summary>
/// Gets or creates a user secrets manager for the specified user secrets ID.
/// </summary>
public IUserSecretsManager GetOrCreateFromId(string? userSecretsId)
{
if (string.IsNullOrWhiteSpace(userSecretsId))
{
return NoopUserSecretsManager.Instance;
}
var filePath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
return GetOrCreate(filePath);
}
/// <summary>
/// Gets or creates a user secrets manager for the assembly with UserSecretsIdAttribute.
/// </summary>
public IUserSecretsManager GetOrCreate(Assembly? assembly)
{
var userSecretsId = assembly?.GetCustomAttribute<UserSecretsIdAttribute>()?.UserSecretsId;
return GetOrCreateFromId(userSecretsId);
}
private sealed class UserSecretsManager : IUserSecretsManager
{
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true };
private readonly SemaphoreSlim _semaphore = new(1, 1);
public UserSecretsManager(string filePath)
{
FilePath = filePath;
}
public string FilePath { get; }
public bool TrySetSecret(string name, string value)
{
try
{
_semaphore.Wait();
try
{
SetSecretCore(name, value);
return true;
}
finally
{
_semaphore.Release();
}
}
catch (Exception)
{
return false;
}
}
public void GetOrSetSecret(IConfigurationManager configuration, string name, Func<string> valueGenerator)
{
var existingValue = configuration[name];
if (existingValue is null)
{
var value = valueGenerator();
configuration.AddInMemoryCollection(
new Dictionary<string, string?>
{
[name] = value
}
);
if (!TrySetSecret(name, value))
{
Debug.WriteLine($"Failed to save value to application user secrets.");
}
}
}
/// <summary>
/// Saves state to user secrets asynchronously (for deployment state manager).
/// If multiple callers save state concurrently, the last write wins.
/// </summary>
public async Task SaveStateAsync(JsonObject state, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var flattenedState = JsonFlattener.FlattenJsonObject(state);
EnsureUserSecretsDirectory();
var json = flattenedState.ToJsonString(s_jsonSerializerOptions);
await File.WriteAllTextAsync(FilePath, json, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}
private void SetSecretCore(string name, string value)
{
EnsureUserSecretsDirectory();
// Load existing secrets, merge with new value, save
var secrets = Load();
secrets[name] = value;
Save(secrets);
}
private Dictionary<string, string?> Load()
{
return new ConfigurationBuilder()
.AddJsonFile(FilePath, optional: true)
.Build()
.AsEnumerable()
.Where(i => i.Value != null)
.ToDictionary(i => i.Key, i => i.Value);
}
private void Save(Dictionary<string, string?> secrets)
{
var contents = new JsonObject();
foreach (var secret in secrets)
{
contents[secret.Key] = secret.Value;
}
var json = contents.ToJsonString(s_jsonSerializerOptions);
// Create a temp file with the correct Unix file mode before moving it to the expected path.
if (!OperatingSystem.IsWindows())
{
var tempFilename = Path.GetTempFileName();
File.WriteAllText(tempFilename, json, Encoding.UTF8);
File.Move(tempFilename, FilePath, overwrite: true);
}
else
{
File.WriteAllText(FilePath, json, Encoding.UTF8);
}
}
private void EnsureUserSecretsDirectory()
{
var directoryName = Path.GetDirectoryName(FilePath);
if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
}
}
}
}
|