Skip to content

[API Proposal]: Support non-locking assembly loading with preserved Assembly.Location #127097

@cdmazom

Description

@cdmazom

Background and motivation

We’re running into an issue with assembly loading in the Unity Editor, and we ended up customizing our fork of the runtime to solve it. Since we've seen other users hit similar roadblocks, we wanted to see if an upstream solution makes sense.

When we load an assembly from a path via AssemblyLoadContext.LoadFromAssemblyPath, the runtime maps the file into memory. On Windows, particularly, this creates an OS-level lock. This interferes with standard Unity Editor workflows as it prevents us (or our users) from performing common operations on those files while the editor is running, like recompiling the assembly, renaming the file, moving it, or deleting it.

The obvious workaround is to read the file into a stream and load it from memory instead. However, this breaks third-party libraries that rely on Assembly.Location (e.g., to find adjacent assets or native dependencies), because loading from a stream leaves the location empty.

Previous Discussions and Related Issues

This is a long-standing challenge within the ecosystem, with several issues highlighting the same friction around file locks and metadata preservation:

API Proposal

We propose adding a static callback to AssemblyLoadContext that allows an application to override the value returned by Assembly.Location. To ensure predictability, this callback should only be settable once for the whole application.

namespace System.Runtime.Loader
{
    public partial class AssemblyLoadContext
    {
        /// <summary>
        /// Sets a global callback to override the value returned by Assembly.Location.
        /// This method can only be called once per application.
        /// </summary>
        /// <param name="locationOverride">
        /// A function that takes the Assembly and its original location (if any), 
        /// and returns the overriding location string.
        /// </param>
        public static void SetAssemblyLocationOverride(Func<Assembly, string, string> locationOverride);
    }
}

API Usage

// 1. Set the callback once during application startup
AssemblyLoadContext.SetAssemblyLocationOverride((assembly, originalLocation) =>
{
    // Consult a custom mapping dictionary (or ALC context) to find the fake location
    if (CustomLoader.TryGetMappedPath(assembly, out string mappedPath))
    {
        return mappedPath;
    }
    
    return originalLocation;
});

// 2. Load the assembly from memory to avoid file locks
var alc = new AssemblyLoadContext("ExampleContext");
var assembly = alc.LoadFromStream(exampleLibStream);

// 3. Register the mapping so the callback returns the expected physical path
CustomLoader.RegisterMapping(assembly, "third-party-lib/example_lib.dll");

// Third-party code can now successfully resolve assembly location
var location = Path.GetDirectoryName(assembly.Location);

Alternative Designs

  • Virtual method on ALC: We also considered allowing AssemblyLoadContext to override it internally via a protected virtual method, e.g., protected internal virtual string ResolveAssemblyLocation(Assembly assembly, string originalLocation). This would scope the override to specific contexts rather than relying on a global static hook and wouldn't work for the default ALC.
  • Dedicated In-Memory Load Method: LoadFromAssemblyPathWithoutLocking(string assemblyPath). This was our original proposal. We discarded it as discussed in this thread to keep the runtime out of the business of managing file-locking trade-offs.

Risks

  • Global State Conflicts: Because the callback is a static, set-once delegate, there is a risk of conflict if multiple independent frameworks or complex hosting scenarios within the same process attempt to claim it.

Changelog

  • Updated: Replaced the LoadFromAssemblyPathWithoutLocking API proposal with a static callback (SetAssemblyLocationOverride) to override assembly resolution, based on feedback regarding runtime responsibilities.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementationarea-AssemblyLoaderuntriagedNew issue has not been triaged by the area owner

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions