File: SupportsDebuggingAnnotation.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.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Indicates that a resource can be launched by an IDE or extension host so it can be debugged,
/// instead of being started as a plain process by Aspire.
/// </summary>
/// <remarks>
/// Added by a <c>WithDebugSupport</c> overload on <see cref="ResourceBuilderExtensions"/>.
/// The annotation is only honored while a debug session is active; use
/// <see cref="DebugSupportExtensions.SupportsDebugging"/> to test for that.
/// </remarks>
[DebuggerDisplay("Type = {GetType().Name,nq}, RequiredExtensionId = {LaunchConfigurationType,nq}")]
[Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class SupportsDebuggingAnnotation : IResourceAnnotation
{
    private SupportsDebuggingAnnotation(
        string launchConfigurationType,
        Func<LaunchConfigurationCallbackContext, Task<object>> launchConfigurationProducer)
    {
        LaunchConfigurationType = launchConfigurationType;
        LaunchConfigurationProducer = launchConfigurationProducer;
    }
 
    /// <summary>
    /// Gets the launch configuration type identifier, for example <see cref="KnownLaunchConfigurationTypes.Project"/>.
    /// </summary>
    /// <remarks>
    /// The IDE advertises the launch configuration types it can handle; a resource whose type is not
    /// advertised is started as a plain process instead.
    /// <para>
    /// Exception: when the active debug session does not
    /// advertise any launch configuration types at all (for example Visual Studio, which does not send a
    /// capability list), <see cref="KnownLaunchConfigurationTypes.Project"/> is treated as implicitly
    /// supported rather than falling back to plain process execution.
    /// </para>
    /// </remarks>
    public string LaunchConfigurationType { get; }
 
    // The producer callback supplied to WithDebugSupport, with the launch configuration boxed as object.
    // Internal because only Aspire constructs LaunchConfigurationCallbackContext values and because the
    // untyped object is consumed by internal launch-configuration plumbing.
    internal Func<LaunchConfigurationCallbackContext, Task<object>> LaunchConfigurationProducer { get; }
 
    internal static SupportsDebuggingAnnotation Create<T>(
        string resourceName,
        string launchConfigurationType,
        Func<LaunchConfigurationCallbackContext, Task<T>> launchConfigurationProducer)
    {
        return new SupportsDebuggingAnnotation(
            launchConfigurationType,
            // The suppression is safe because ProduceAsync throws rather than returning null; the
            // compiler cannot see that because T is unconstrained and so may be a nullable type.
            async context => (await ProduceAsync(context).ConfigureAwait(false))!);
 
        async Task<T> ProduceAsync(LaunchConfigurationCallbackContext context)
        {
            var launchConfiguration = await launchConfigurationProducer(context).ConfigureAwait(false);
            if (launchConfiguration is null)
            {
                throw new InvalidOperationException(
                    $"The \"{launchConfigurationType}\" launch configuration producer for resource '{resourceName}' returned null. " +
                    $"The producer owns the complete launch configuration, so it must always return one.");
            }
 
            return launchConfiguration;
        }
    }
}