Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API for resolving component dependencies #27787

Closed
vitek-karas opened this issue Oct 31, 2018 · 2 comments
Closed

Add API for resolving component dependencies #27787

vitek-karas opened this issue Oct 31, 2018 · 2 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Milestone

Comments

@vitek-karas
Copy link
Member

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.
@vitek-karas vitek-karas self-assigned this Oct 31, 2018
@vitek-karas
Copy link
Member Author

Working on the implementation of this API. Current progress here.

@terrajobst
Copy link
Member

terrajobst commented Dec 18, 2018

Video

  • Should we offer a resolver that represents the "global" context? AssemblyLoadContext kind of does that, but it doesn't talk in paths.
  • The term component seems a bit ill-defined. AssemblyDependencyResolver seems to make sense, given that this type is for resolving dependencies of assemblies (managed or unamanged). The only caveat is that you need to pass the path to the assembly that has the corresponding <assembly>.deps.json file, i.e. the main assembly or the entry point of the plug-in.
namespace System.Runtime.Loader
{
    public sealed class AssemblyDependencyResolver
    {
        // Should this API exist?
        // public static AssemblyDependencyResolver GlobalResolver { get; }

        public AssemblyDependencyResolver(string assemblyPath);

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

@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 3.0 milestone Jan 31, 2020
@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 15, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Projects
None yet
Development

No branches or pull requests

3 participants