Skip to content

Add API for resolving component dependencies #27787

@vitek-karas

Description

@vitek-karas

This is a proposal to add new public API which would expose functionality to help with resolution of managed and unmanaged dependencies of components.

Proposed Surface Area

namespace System.Runtime.Loader
{
    public sealed class ComponentDependencyResolver
    {
        public ComponentDependencyResolver(string componentAssemblyPath);

        public string ResolveAssemblyToPath(AssemblyName assemblyName);
        public string ResolveUnmanagedDllToPath(string unmanagedDllName);
    }
}

Functionality

Given the path to a component assembly (the main .dll of a given component, for example the build result of a class library project), the constructor creates a resolver object which can resolve managed and unmanaged dependencies of the component. The constructor would look for the .deps.json file next to the main assembly and use it to compute the set of dependencies.

The ResolveAssemblyToPath and ResolveUnmanagedDllToPath methods are then used to resolve references to managed and unmanaged dependencies. These methods take the name of the dependency and return either null if such dependency can't be resolved by the component, or a full path to the file (managed assembly or unmanaged library).

The constructor is expected to catch most error cases and report them as exceptions. The Resolve methods should in general not throw and instead return null if the dependency can't be resolved.

Scenario: Dynamic component loading

The proposed API can be used to greatly simplify dynamic loading of components. It provides a powerful building block to use for implementing custom AssemblyLoadContext or event handlers for the binding events like AppDomain.AssemblyResolve and AssemblyLoadContext.Resolving.

Example of using the new API to load plugins with AssemblyLoadContext in isolation:

class PluginLoadContext : AssemblyLoadContext
{
    ComponentDependencyResolver _resolver;
    
    public PluginLoadContext(string pluginPath)
    {
        _resolver = new ComponentDependencyResolver(pluginPath);
    }

    public override Assembly Load(AssemblyName assemblyName)
    {
        string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
        if (assemblyPath != null)
        {
            return LoadFromAssemblyPath(assemblyPath);
        }
    
        return null;
    }
}

PluginLoadContext pluginContext = new PluginLoadContext("/pathtoplugin/plugin.dll");
Assembly pluginAssembly = pluginContext.LoadFromAssemblyName(new AssemblyName("Plugin"));

// ... use the pluginAssembly and reflection to invoke functionality from the plugin.
// Dependencies of the plugin are resolved by the event handler above using the resolver
// to provide the actual resolution from assembly name to file path.

Scenario: Inspecting IL metadata of components

Using the newly proposed MetadataLoadContext API (see the proposal) to inspect IL metadata of components. This API requires an assembly resolver to resolve dependencies of the component. The proposed ComponentDependencyResolver would be used to implement such resolver for components produced by the .NET Core SDK.

Example of using the new API to implement MetadataAssemblyResolver:

public class ComponentMetadataAssemblyResolver : MetadataAssemblyResolver
{
    private ComponentDependencyResolver dependencyResolver;

    public override Assembly Resolve(MetadataLoadContext context, AssemblyName assemblyName)
    {
        string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName);
        if (assemblyPath != null)
        {
            return context.LoadFromAssemblyPath(assemblyPath);
        }

        // Code to load framework dependencies from the running app for example
        // using Assembly.Load and so on.

        return null;
    }
}

Context

.NET Core SDK (used by VS, VS Code, VS for Mac and so on) describes component dependencies in the build output via the .deps.json files (description). These files are consumed by the hosting components (dotnet.exe or the app's executable) and they're used to compute the list of dependencies needed to run the application. This happens at startup and through this mechanism all static dependencies of the app are resolved.

Currently there's no such mechanism for components which are loaded dynamically. Applications can use Microsoft.Extensions.DependencyModel package which provides object model of the .deps.json file, but it's relatively complex to use this for dependency resolution. It's also very likely that the behavior of such custom solution would be somewhat different from what the hosting layer does for static dependencies.

Open issues

  • Naming - The use of Component in the class name was chosen to differentiate from Assembly as the proposed API will only work on entire components produced by the SDK. Using Assembly seems to mean that the API would inspect the assembly itself to determine its dependencies, which is not the purpose of this API. That said it could be either. Also using the term Resolver can be seen as somewhat misleading. Resolve in the context of assembly binding typically means to find and actually load the dependency. The purpose of this API is to simply find the file, not to load it. So maybe it should be more explicit by using for example PathResolver. Candidates then could be AssemblyDepednencyResolver, AssemblyDependencyPathResolver, ComponentDependencyPathResolver.

Notes

  • As proposed the resolver would not resolve framework dependencies. For the typical case of dynamically loaded component, resolving framework dependencies would actually just introduce more issues and probably provide unwanted behavior. This is something we would look into in the future, as it's an important scenario for the MetadataLoadContext.
  • The resolver has no ties to the runtime. This means that the dependencies are resolved to file paths without any consideration to what assemblies are already loaded into the application. This is important for scenarios where full isolation is required. For partial or no isolation scenarios it is expected that the loading of the dependencies will be combined with the appropriate fallback to the default load context.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions