File: IntegrationLoadContext.cs
Web Access
Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.RemoteHost;
 
/// <summary>
/// Loads Aspire integration assemblies from probe directories with version unification.
/// If the default load context already provides an assembly at a higher or equal version,
/// the default context wins. Only <c>Aspire.TypeSystem</c> is always shared.
/// </summary>
internal sealed class IntegrationLoadContext : AssemblyLoadContext
{
    private const string SharedAssemblyName = "Aspire.TypeSystem";
 
    /// <summary>
    /// Gets the assembly names that this load context shares with the default
    /// <see cref="AssemblyLoadContext"/> (resolution always defers to the default ALC).
    /// </summary>
    internal static IReadOnlyList<string> GetSharedAssemblyNames() => [SharedAssemblyName];
 
    private readonly string[] _probeDirectories;
    private readonly IntegrationPackageProbeManifest _packageProbeManifest;
    private readonly ILogger _logger;
 
    internal IntegrationLoadContext(string[] probeDirectories, ILogger logger)
        : this(probeDirectories, IntegrationPackageProbeManifest.Empty, logger)
    {
    }
 
    internal IntegrationLoadContext(string[] probeDirectories, IntegrationPackageProbeManifest packageProbeManifest, ILogger logger)
        : base("Aspire.Integrations")
    {
        _probeDirectories = probeDirectories;
        _packageProbeManifest = packageProbeManifest;
        _logger = logger;
    }
 
    protected override Assembly? Load(AssemblyName assemblyName)
    {
        if (assemblyName.Name is null ||
            string.Equals(assemblyName.Name, SharedAssemblyName, StringComparison.OrdinalIgnoreCase))
        {
            // Returning null tells the runtime to fall back to the default ALC.
            // This ensures Aspire.TypeSystem has the same type identity in both
            // contexts, so shared contracts (ICodeGenerator, ILanguageSupport,
            // AtsContext, etc.) work across the ALC boundary without requiring
            // reflection or marshalling.
            //
            // Binding to the bundled (CLI) copy is safe because Aspire.TypeSystem freezes its
            // strong-name AssemblyVersion at a fixed constant so the bundled copy satisfies the
            // strong-named reference of any stable SDK codegen assembly (#18110, #17910). See
            // src/Aspire.TypeSystem/Aspire.TypeSystem.csproj for the full rationale and the one
            // residual "update your CLI" case routed to the #18125 diagnostics.
            return null;
        }
 
        var probedPath = _packageProbeManifest.TryGetManagedAssemblyPath(assemblyName)
            ?? TryResolveFromProbeDirectories(assemblyName.Name);
 
        if (probedPath is null)
        {
            return null;
        }
 
        // Version unification: if the default context already has this assembly
        // at a higher or equal version (e.g., framework-provided), defer to it.
        if (TryGetDefaultContextVersion(assemblyName, out var defaultVersion))
        {
            var probedVersion = AssemblyName.GetAssemblyName(probedPath).Version;
            if (defaultVersion >= probedVersion)
            {
                _logger.LogDebug("[IntegrationALC] Deferring to default ({DefaultVersion} >= {ProbedVersion}): {AssemblyName}",
                    defaultVersion, probedVersion, assemblyName.Name);
                return null;
            }
        }
 
        _logger.LogDebug("[IntegrationALC] Loading: {AssemblyName} from {Path}", assemblyName.Name, probedPath);
        return LoadFromAssemblyPath(probedPath);
    }
 
    protected override nint LoadUnmanagedDll(string unmanagedDllName)
    {
        foreach (var path in _packageProbeManifest.GetNativeLibraryPaths(unmanagedDllName))
        {
            try
            {
                _logger.LogDebug("[IntegrationALC] Loading native library: {NativeLibraryName} from {Path}", unmanagedDllName, path);
                return LoadUnmanagedDllFromPath(path);
            }
            catch (DllNotFoundException ex)
            {
                _logger.LogDebug(ex, "[IntegrationALC] Failed to load native library {NativeLibraryName} from {Path}", unmanagedDllName, path);
            }
            catch (BadImageFormatException ex)
            {
                _logger.LogDebug(ex, "[IntegrationALC] Failed to load native library {NativeLibraryName} from {Path}", unmanagedDllName, path);
            }
            catch (FileLoadException ex)
            {
                _logger.LogDebug(ex, "[IntegrationALC] Failed to load native library {NativeLibraryName} from {Path}", unmanagedDllName, path);
            }
            catch (IOException ex)
            {
                _logger.LogDebug(ex, "[IntegrationALC] Failed to load native library {NativeLibraryName} from {Path}", unmanagedDllName, path);
            }
            catch (UnauthorizedAccessException ex)
            {
                _logger.LogDebug(ex, "[IntegrationALC] Failed to load native library {NativeLibraryName} from {Path}", unmanagedDllName, path);
            }
        }
 
        return base.LoadUnmanagedDll(unmanagedDllName);
    }
 
    private static bool TryGetDefaultContextVersion(AssemblyName assemblyName, out Version? version)
    {
        version = null;
        try
        {
            var defaultAsm = Default.LoadFromAssemblyName(assemblyName);
            version = defaultAsm.GetName().Version;
            return version is not null;
        }
        catch
        {
            return false;
        }
    }
 
    private string? TryResolveFromProbeDirectories(string assemblyName)
    {
        foreach (var dir in _probeDirectories)
        {
            var path = Path.Combine(dir, $"{assemblyName}.dll");
            if (File.Exists(path))
            {
                return path;
            }
        }
 
        return null;
    }
}