File: Language\LanguageSupportResolver.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 Aspire.TypeSystem;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.RemoteHost.Language;
 
/// <summary>
/// Resolves language support implementations by language, discovering them from loaded assemblies.
/// </summary>
internal sealed class LanguageSupportResolver
{
    private readonly Lazy<Dictionary<string, ILanguageSupport>> _languages;
    private readonly ILogger<LanguageSupportResolver> _logger;
 
    public LanguageSupportResolver(
        IServiceProvider serviceProvider,
        AssemblyLoader assemblyLoader,
        ILogger<LanguageSupportResolver> logger)
        : this(serviceProvider, assemblyLoader.GetAssemblies, logger)
    {
    }
 
    // Test-only seam: lets unit tests inject a synthetic assembly set without going
    // through the AssemblyLoader (which is sealed and probes the file system).
    internal LanguageSupportResolver(
        IServiceProvider serviceProvider,
        Func<IReadOnlyList<Assembly>> assembliesProvider,
        ILogger<LanguageSupportResolver> logger)
    {
        _logger = logger;
        _languages = new Lazy<Dictionary<string, ILanguageSupport>>(
            () => DiscoverLanguages(serviceProvider, assembliesProvider()));
    }
 
    /// <summary>
    /// Gets language support for the specified language.
    /// </summary>
    /// <param name="language">The target language (e.g., "TypeScript", "Python").</param>
    /// <returns>The language support, or null if not found.</returns>
    public ILanguageSupport? GetLanguageSupport(string language)
    {
        _languages.Value.TryGetValue(language, out var support);
        return support;
    }
 
    /// <summary>
    /// Gets all available language support implementations.
    /// </summary>
    /// <returns>All discovered language support implementations.</returns>
    public IEnumerable<ILanguageSupport> GetAllLanguages()
    {
        return _languages.Value.Values;
    }
 
    private Dictionary<string, ILanguageSupport> DiscoverLanguages(
        IServiceProvider serviceProvider,
        IReadOnlyList<Assembly> assemblies)
    {
        var languages = new Dictionary<string, ILanguageSupport>(StringComparer.OrdinalIgnoreCase);
 
        foreach (var assembly in assemblies)
        {
            Type[] types;
            var assemblyName = assembly.GetName().Name;
            var hadTypeLoadFailure = false;
            try
            {
                types = assembly.GetTypes();
            }
            catch (ReflectionTypeLoadException ex)
            {
                hadTypeLoadFailure = true;
                // Surface loader binding failures at Warning level. These typically indicate
                // a binary mismatch between the bundled runtime assemblies and the integration
                // assemblies loaded from disk (for example, when Aspire.TypeSystem versions
                // diverge). Including the LoaderExceptions in the log is essential for
                // diagnosing these failures, which previously disappeared into Debug-level
                // output that the apphost server never wrote to disk.
                var loaderMessages = ex.LoaderExceptions is { Length: > 0 } loaders
                    ? string.Join("; ", loaders.Where(e => e is not null).Select(e => e!.Message).Distinct())
                    : "(no LoaderExceptions captured)";
                _logger.LogWarning(
                    ex,
                    "Some types in assembly '{AssemblyName}' could not be loaded; {LoadedCount} of {TotalCount} types are available. LoaderExceptions: {LoaderExceptions}",
                    assemblyName,
                    ex.Types.Count(t => t is not null),
                    ex.Types.Length,
                    loaderMessages);
                // Use the types that were successfully loaded
                types = ex.Types.Where(t => t is not null).ToArray()!;
            }
 
            var discoveredInAssembly = 0;
            foreach (var type in types)
            {
                if (!type.IsAbstract &&
                    !type.IsInterface &&
                    typeof(ILanguageSupport).IsAssignableFrom(type))
                {
                    try
                    {
                        var language = (ILanguageSupport)ActivatorUtilities.CreateInstance(serviceProvider, type);
                        languages[language.Language] = language;
                        discoveredInAssembly++;
                        _logger.LogDebug("Discovered language support: {TypeName} for language '{Language}'", type.Name, language.Language);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "Failed to instantiate language support '{TypeName}'", type.Name);
                    }
                }
            }
 
            // If an assembly named like a code-generation / language-support contributor
            // produced zero implementations, that is almost certainly a silent type-load
            // failure rather than an intentional design. Log a Warning so the user can see it.
            if (discoveredInAssembly == 0 && LooksLikeLanguageSupportAssembly(assemblyName))
            {
                _logger.LogWarning(
                    "Assembly '{AssemblyName}' was loaded but did not contribute any {Interface} implementations. {Hint}",
                    assemblyName,
                    nameof(ILanguageSupport),
                    hadTypeLoadFailure
                        ? "This is likely caused by a binary mismatch between the bundled and probed assemblies (see preceding LoaderExceptions)."
                        : "Verify the assembly contains a non-abstract type that implements " + typeof(ILanguageSupport).FullName + ".");
            }
        }
 
        return languages;
    }
 
    private static bool LooksLikeLanguageSupportAssembly(string? assemblyName)
        => assemblyName is not null
           && assemblyName.StartsWith("Aspire.Hosting.CodeGeneration.", StringComparison.OrdinalIgnoreCase);
}