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.
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:
AssemblyLoadContextthat reads files into memory and releases the file locks on disk immediately.Assembly.Locationcauses plugins likeSteamworks.NETto fail when resolving native dependencies (libsteam_api.so).API Proposal
We propose adding a static callback to
AssemblyLoadContextthat allows an application to override the value returned byAssembly.Location. To ensure predictability, this callback should only be settable once for the whole application.API Usage
Alternative Designs
AssemblyLoadContextto 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.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
Changelog
LoadFromAssemblyPathWithoutLockingAPI proposal with a static callback (SetAssemblyLocationOverride) to override assembly resolution, based on feedback regarding runtime responsibilities.