File: BrowserConfiguration.cs
Web Access
Project: src\src\Aspire.Hosting.Browsers\Aspire.Hosting.Browsers.csproj (Aspire.Hosting.Browsers)
// 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 ASPIREBROWSERLOGS001 // Type is for evaluation purposes only
 
using System.Globalization;
using Aspire.Hosting.Browsers.Resources;
using Microsoft.Extensions.Configuration;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Resolved browser configuration used for one tracked browser session.
/// </summary>
/// <remarks>
/// Resolution keeps "which browser/profile did the caller ask for?" separate from "which user data directory
/// does that imply?". The later user-data-directory decision belongs to <see cref="BrowserHostRegistry"/>, where
/// the resolved browser executable path is available.
/// </remarks>
internal readonly record struct BrowserConfiguration(
    string Browser,
    string? Profile,
    BrowserUserDataMode UserDataMode,
    string? AppHostKey)
{
    /// <summary>
    /// The default mode points at an Aspire-managed persistent user data directory shared across every Aspire
    /// AppHost on the machine, so cookies, sign-ins, and extensions persist between runs.
    /// </summary>
    internal const BrowserUserDataMode DefaultUserDataMode = BrowserUserDataMode.Shared;
 
    /// <summary>
    /// Resolves explicit method arguments, resource-scoped configuration, global configuration, and defaults.
    /// </summary>
    internal static BrowserConfiguration Resolve(
        IConfiguration configuration,
        string resourceName,
        BrowserConfigurationExplicitValues explicitValues,
        BrowserConfiguration? resourceRuntimeConfiguration = null,
        BrowserConfiguration? globalRuntimeConfiguration = null)
    {
        ArgumentNullException.ThrowIfNull(configuration);
        ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
 
        var browserLogsSection = configuration.GetSection(BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName);
        var resourceSection = browserLogsSection.GetSection(resourceName);
 
        // Resolution order is explicit argument -> resource-specific config -> global browser-log config -> default.
        // Resolve user-data mode before browser so the browser default can prefer Edge for shared state and Chrome for
        // disposable isolated state.
        var resolvedProfile = ResolveProfile(explicitValues, resourceRuntimeConfiguration, globalRuntimeConfiguration, resourceSection, browserLogsSection);
        var resolvedUserDataMode = ResolveUserDataMode(explicitValues, resourceRuntimeConfiguration, globalRuntimeConfiguration, resourceSection, browserLogsSection);
        var resolvedBrowser = ResolveBrowser(explicitValues, resourceRuntimeConfiguration, globalRuntimeConfiguration, resourceSection, browserLogsSection, resolvedUserDataMode);
 
        if (string.IsNullOrWhiteSpace(resolvedBrowser))
        {
            throw new InvalidOperationException(BrowserMessageStrings.BrowserLogsEmptyBrowserConfiguration);
        }
 
        if (resolvedProfile is not null && string.IsNullOrWhiteSpace(resolvedProfile))
        {
            throw new InvalidOperationException(BrowserMessageStrings.BrowserLogsEmptyProfileConfiguration);
        }
 
        if (resolvedUserDataMode == BrowserUserDataMode.Isolated && resolvedProfile is not null)
        {
            throw new InvalidOperationException(
                string.Format(
                    CultureInfo.CurrentCulture,
                    BrowserMessageStrings.BrowserLogsProfileRequiresSharedUserDataMode,
                    BrowserLogsBuilderExtensions.ProfileConfigurationKey,
                    resolvedProfile,
                    BrowserLogsBuilderExtensions.UserDataModeConfigurationKey,
                    BrowserUserDataMode.Isolated,
                    BrowserUserDataMode.Shared));
        }
 
        // Stable per-AppHost key sourced from DistributedApplicationBuilder. Only Isolated mode actually needs it
        // (its user-data path includes the AppHost segment), but it is always captured here so the registry never
        // has to re-read configuration. The same SHA value is used for other per-AppHost persisted state.
        var appHostKey = configuration["AppHost:PathSha256"];
 
        return new BrowserConfiguration(resolvedBrowser, resolvedProfile, resolvedUserDataMode, appHostKey);
    }
 
    /// <summary>
    /// Selects the default browser for the default user data mode.
    /// </summary>
    internal static string GetDefaultBrowser(Func<string, string?> resolveBrowserExecutable) =>
        GetDefaultBrowser(DefaultUserDataMode, resolveBrowserExecutable);
 
    /// <summary>
    /// Selects the default browser for the effective user data mode.
    /// </summary>
    internal static string GetDefaultBrowser(BrowserUserDataMode userDataMode, Func<string, string?> resolveBrowserExecutable)
    {
        if (userDataMode == BrowserUserDataMode.Shared &&
            resolveBrowserExecutable("msedge") is not null)
        {
            return "msedge";
        }
 
        if (resolveBrowserExecutable("chrome") is not null)
        {
            return "chrome";
        }
 
        if (resolveBrowserExecutable("msedge") is not null)
        {
            return "msedge";
        }
 
        return "chrome";
    }
 
    private static ConfigurationValue<BrowserUserDataMode> ParseUserDataMode(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return ConfigurationValue<BrowserUserDataMode>.Missing;
        }
 
        if (Enum.TryParse<BrowserUserDataMode>(value, ignoreCase: true, out var parsed))
        {
            return ConfigurationValue<BrowserUserDataMode>.Present(parsed);
        }
 
        throw new InvalidOperationException(
            string.Format(
                CultureInfo.CurrentCulture,
                BrowserMessageStrings.BrowserLogsInvalidUserDataModeConfiguration,
                value,
                BrowserLogsBuilderExtensions.UserDataModeConfigurationKey,
                BrowserUserDataMode.Shared,
                BrowserUserDataMode.Isolated));
    }
 
    private static string GetDefaultBrowser(BrowserUserDataMode userDataMode) =>
        GetDefaultBrowser(userDataMode, ChromiumBrowserResolver.TryResolveExecutable);
 
    private static string? ResolveProfile(
        BrowserConfigurationExplicitValues explicitValues,
        BrowserConfiguration? resourceRuntimeConfiguration,
        BrowserConfiguration? globalRuntimeConfiguration,
        IConfigurationSection resourceSection,
        IConfigurationSection browserLogsSection)
        => ResolveValue(
            FromOptionalString(explicitValues.Profile),
            resourceRuntimeConfiguration,
            globalRuntimeConfiguration,
            static configuration => configuration.Profile,
            resourceSection,
            browserLogsSection,
            BrowserLogsBuilderExtensions.ProfileConfigurationKey,
            FromOptionalString,
            static () => null);
 
    private static BrowserUserDataMode ResolveUserDataMode(
        BrowserConfigurationExplicitValues explicitValues,
        BrowserConfiguration? resourceRuntimeConfiguration,
        BrowserConfiguration? globalRuntimeConfiguration,
        IConfigurationSection resourceSection,
        IConfigurationSection browserLogsSection)
        => ResolveValue(
            explicitValues.UserDataMode is { } explicitUserDataMode
                ? ConfigurationValue<BrowserUserDataMode>.Present(explicitUserDataMode)
                : ConfigurationValue<BrowserUserDataMode>.Missing,
            resourceRuntimeConfiguration,
            globalRuntimeConfiguration,
            static configuration => configuration.UserDataMode,
            resourceSection,
            browserLogsSection,
            BrowserLogsBuilderExtensions.UserDataModeConfigurationKey,
            ParseUserDataMode,
            static () => DefaultUserDataMode);
 
    private static string ResolveBrowser(
        BrowserConfigurationExplicitValues explicitValues,
        BrowserConfiguration? resourceRuntimeConfiguration,
        BrowserConfiguration? globalRuntimeConfiguration,
        IConfigurationSection resourceSection,
        IConfigurationSection browserLogsSection,
        BrowserUserDataMode resolvedUserDataMode)
        => ResolveValue<string?>(
            FromOptionalString(explicitValues.Browser),
            resourceRuntimeConfiguration,
            globalRuntimeConfiguration,
            static configuration => configuration.Browser,
            resourceSection,
            browserLogsSection,
            BrowserLogsBuilderExtensions.BrowserConfigurationKey,
            FromOptionalString,
            () => GetDefaultBrowser(resolvedUserDataMode))!;
 
    private static T ResolveValue<T>(
        ConfigurationValue<T> explicitValue,
        BrowserConfiguration? resourceRuntimeConfiguration,
        BrowserConfiguration? globalRuntimeConfiguration,
        Func<BrowserConfiguration, T> getRuntimeValue,
        IConfigurationSection resourceSection,
        IConfigurationSection browserLogsSection,
        string configurationKey,
        Func<string?, ConfigurationValue<T>> parseConfigurationValue,
        Func<T> getDefault)
    {
        if (explicitValue.HasValue)
        {
            return explicitValue.Value;
        }
 
        if (resourceRuntimeConfiguration is { } resourceRuntime)
        {
            return getRuntimeValue(resourceRuntime);
        }
 
        var resourceConfiguration = parseConfigurationValue(resourceSection[configurationKey]);
        if (resourceConfiguration.HasValue)
        {
            return resourceConfiguration.Value;
        }
 
        if (globalRuntimeConfiguration is { } globalRuntime)
        {
            return getRuntimeValue(globalRuntime);
        }
 
        var globalConfiguration = parseConfigurationValue(browserLogsSection[configurationKey]);
        return globalConfiguration.HasValue
            ? globalConfiguration.Value
            : getDefault();
    }
 
    private static ConfigurationValue<string?> FromOptionalString(string? value) =>
        value is null ? ConfigurationValue<string?>.Missing : ConfigurationValue<string?>.Present(value);
 
    private readonly record struct ConfigurationValue<T>(bool HasValue, T Value)
    {
        public static ConfigurationValue<T> Missing => new(false, default!);
 
        public static ConfigurationValue<T> Present(T value) => new(true, value);
    }
}
 
/// <summary>
/// Browser configuration values explicitly supplied by the resource builder.
/// </summary>
internal readonly record struct BrowserConfigurationExplicitValues(
    string? Browser = null,
    string? Profile = null,
    BrowserUserDataMode? UserDataMode = null);