File: Sdk\SdkResolver.cs
Web Access
Project: src\msbuild\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;

namespace Microsoft.Build.Framework
{
    /// <summary>
    /// An abstract interface for classes that can resolve a Software Development Kit (SDK).
    /// </summary>
    public abstract class SdkResolver
    {
        /// <summary>
        /// Gets the name of the <see cref="SdkResolver"/> to be displayed in build output log.
        /// </summary>
        public abstract string Name { get; }

        /// <summary>
        /// Gets the self-described resolution priority order. MSBuild will sort resolvers
        /// by this value.
        /// </summary>
        public abstract int Priority { get; }

        /// <summary>
        /// Resolves the specified SDK reference.
        /// </summary>
        /// <param name="sdkReference">A <see cref="SdkReference" /> containing the referenced SDKs be resolved.</param>
        /// <param name="resolverContext">Context for resolving the SDK.</param>
        /// <param name="factory">Factory class to create an <see cref="SdkResult" /></param>
        /// <returns>
        /// An <see cref="SdkResult" /> containing the resolved SDKs or associated error / reason
        /// the SDK could not be resolved.  Return <see langword="null"/> if the resolver is not
        /// applicable for a particular <see cref="SdkReference"/>.
        /// </returns>
        /// <remarks>
        /// Note: You must use <see cref="SdkResultFactory"/> to return a result.
        /// </remarks>
        public abstract SdkResult? Resolve(SdkReference sdkReference,
                                          SdkResolverContext resolverContext,
                                          SdkResultFactory factory);

        /// <summary>
        /// The set of resolvers registered in-process via <see cref="Register"/>.
        /// </summary>
        private static readonly List<SdkResolver> s_registeredResolvers = new();

        /// <summary>
        /// Registers an <see cref="SdkResolver"/> to be consulted during SDK resolution by a host that runs
        /// the MSBuild engine in-process (for example the .NET SDK CLI), without MSBuild discovering and
        /// loading it from disk by reflection.
        /// </summary>
        /// <param name="resolver">The resolver instance to register.</param>
        /// <remarks>
        /// <para>
        /// This is the supported way to provide SDK resolvers in a trimmed or Native AOT host, where the
        /// on-disk <c>SdkResolvers</c> probing and reflection-based loading used for plugin resolvers are
        /// unavailable. The registered resolver is consulted on the same reflection-free code path as
        /// MSBuild's built-in resolver, so it never triggers the dynamic-loading failure (MSB4282).
        /// </para>
        /// <para>
        /// The registered resolver participates in resolution in <see cref="Priority"/> order alongside
        /// MSBuild's built-in resolver, with no assembly loading or reflection. It is consulted for every
        /// SDK reference in the process.
        /// </para>
        /// <para>
        /// Intended to be called once per resolver during host initialization, before the first project is
        /// evaluated. The set of registered resolvers is captured the first time an SDK is resolved in the
        /// process; registrations performed after that point are not guaranteed to take effect.
        /// </para>
        /// <para>
        /// This method is thread-safe. Registering the same instance more than once has no additional effect.
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="resolver"/> is <see langword="null"/>.</exception>
        public static void Register(SdkResolver resolver)
        {
            if (resolver is null)
            {
                throw new ArgumentNullException(nameof(resolver));
            }

            lock (s_registeredResolvers)
            {
                bool alreadyRegistered = false;
                foreach (SdkResolver registeredResolver in s_registeredResolvers)
                {
                    if (ReferenceEquals(registeredResolver, resolver))
                    {
                        alreadyRegistered = true;
                        break;
                    }
                }

                if (!alreadyRegistered)
                {
                    s_registeredResolvers.Add(resolver);
                }
            }
        }

        /// <summary>
        /// Gets a snapshot of the resolvers registered via <see cref="Register"/>, for the engine to fold
        /// into its reflection-free default-resolver pass.
        /// </summary>
        internal static IReadOnlyList<SdkResolver> RegisteredResolvers
        {
            get
            {
                lock (s_registeredResolvers)
                {
                    return s_registeredResolvers.Count == 0
                        ? Array.Empty<SdkResolver>()
                        : s_registeredResolvers.ToArray();
                }
            }
        }

        /// <summary>
        /// Clears all registered resolvers. For test use only, to reset the process-global registration state.
        /// </summary>
        internal static void ClearRegisteredResolversForTests()
        {
            lock (s_registeredResolvers)
            {
                s_registeredResolvers.Clear();
            }
        }
    }
}