File: UserSecrets\UserSecretsManagerFactory.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only
#pragma warning disable ASPIREUSERSECRETS001
 
using System.Diagnostics;
using System.Reflection;
using System.Text.Json.Nodes;
using Aspire.Hosting.Pipelines.Internal;
using Aspire.Shared.UserSecrets;
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
{
    // Dictionary to cache instances by file path
    private readonly Dictionary<string, IUserSecretsManager> _managerCache = new();
    private readonly object _lock = new();
 
    internal UserSecretsManagerFactory(IFileSystemService fileSystemService)
    {
        ArgumentNullException.ThrowIfNull(fileSystemService);
    }
 
    /// <summary>
    /// Gets or creates a user secrets manager for the specified file path.
    /// </summary>
    public IUserSecretsManager GetOrCreate(string filePath, string? legacyUserSecretsFilePath = null)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
 
        var normalizedPath = Path.GetFullPath(filePath);
        var normalizedLegacyPath = string.IsNullOrWhiteSpace(legacyUserSecretsFilePath) ? null : Path.GetFullPath(legacyUserSecretsFilePath);
        var cacheKey = normalizedLegacyPath is null ? normalizedPath : $"{normalizedPath}|{normalizedLegacyPath}";
 
        lock (_lock)
        {
            if (!_managerCache.TryGetValue(cacheKey, out var manager))
            {
                manager = new UserSecretsManager(normalizedPath, normalizedLegacyPath);
                _managerCache[cacheKey] = 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 readonly SemaphoreSlim _semaphore = new(1, 1);
        private readonly string? _legacyUserSecretsFilePath;
 
        public UserSecretsManager(string filePath, string? legacyUserSecretsFilePath)
        {
            FilePath = filePath;
            _legacyUserSecretsFilePath = legacyUserSecretsFilePath;
        }
 
        public bool IsAvailable => true;
 
        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 bool TryDeleteSecret(string name)
        {
            try
            {
                _semaphore.Wait();
                try
                {
                    DeleteSecretCore(FilePath, name);
                    if (_legacyUserSecretsFilePath is not null && !PathsEqual(_legacyUserSecretsFilePath, FilePath))
                    {
                        DeleteSecretCore(_legacyUserSecretsFilePath, name);
                    }
                    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);
                SecretsStore.SaveJson(FilePath, flattenedState);
            }
            finally
            {
                _semaphore.Release();
            }
        }
 
        private void SetSecretCore(string name, string value)
        {
            var store = new SecretsStore(FilePath);
            store.Set(name, value);
            store.Save();
        }
 
        private static void DeleteSecretCore(string filePath, string name)
        {
            var store = new SecretsStore(filePath);
            if (store.Remove(name))
            {
                store.Save();
            }
        }
 
        private static bool PathsEqual(string left, string right) =>
            string.Equals(Path.GetFullPath(left), Path.GetFullPath(right), OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
    }
}