diff --git a/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs b/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs
index 48ebce7eb9e4cc..ea39af1c950455 100644
--- a/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs
+++ b/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs
@@ -519,7 +519,7 @@ public override string Location
RuntimeAssembly runtimeAssembly = this;
GetLocation(new QCallAssembly(ref runtimeAssembly), new StringHandleOnStack(ref location));
- return location!;
+ return AssemblyLoadContext.ResolveAssemblyLocation(this, location!);
}
}
diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs
index 2389a9bc0d6a84..da530bc1d1f6d8 100644
--- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs
+++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs
@@ -63,7 +63,7 @@ public override string Location
{
get
{
- return string.Empty;
+ return System.Runtime.Loader.AssemblyLoadContext.ResolveAssemblyLocation(this, string.Empty);
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
index ac51112ce4dbd1..d8d8ba8e142ffd 100644
--- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
+++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
@@ -4052,6 +4052,9 @@
Use of ResourceManager for custom types is disabled. Set the MSBuild Property CustomResourceTypesSupport to true in order to enable it.
+
+ The assembly location override has already been set.
+
The assembly can not be edited or changed.
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs
index 569e1c039d3b18..8064d2a46a7bd1 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs
@@ -309,6 +309,45 @@ public static AssemblyName GetAssemblyName(string assemblyPath)
return AssemblyName.GetAssemblyName(assemblyPath);
}
+ // Callback that, when set, can override the value returned by Assembly.Location.
+ private static Func? s_assemblyLocationOverride;
+
+ ///
+ /// Sets a process-wide callback that overrides the value returned by .
+ ///
+ ///
+ /// The callback can only be set once for the lifetime of the process. The callback should not
+ /// call on the provided assembly to avoid recursion.
+ ///
+ ///
+ /// A callback that receives an and the location the runtime computed for it, and
+ /// returns the value that should report.
+ ///
+ /// is .
+ /// The location override has already been set.
+ public static void SetAssemblyLocationOverride(Func locationOverride)
+ {
+ ArgumentNullException.ThrowIfNull(locationOverride);
+
+ if (Interlocked.CompareExchange(ref s_assemblyLocationOverride, locationOverride, null) is not null)
+ {
+ throw new InvalidOperationException(SR.InvalidOperation_AssemblyLocationOverrideAlreadySet);
+ }
+ }
+
+ // Applies the location override callback (if any) to the location the runtime computed for the assembly.
+ // Called from each runtime's RuntimeAssembly.Location implementation.
+ internal static string ResolveAssemblyLocation(Assembly assembly, string originalLocation)
+ {
+ Func? locationOverride = s_assemblyLocationOverride;
+ if (locationOverride is null)
+ {
+ return originalLocation;
+ }
+
+ return locationOverride(assembly, originalLocation);
+ }
+
// Custom AssemblyLoadContext implementations can override this
// method to perform custom processing and use one of the protected
// helpers above to load the assembly.
diff --git a/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs b/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs
index 8f7555ac5b599d..3ad39352c54e5f 100644
--- a/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs
+++ b/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs
@@ -87,6 +87,7 @@ public event System.Action? Unloading
public System.Reflection.Assembly LoadFromStream(System.IO.Stream assembly, System.IO.Stream? assemblySymbols) { throw null; }
protected virtual System.IntPtr LoadUnmanagedDll(string unmanagedDllName) { throw null; }
protected System.IntPtr LoadUnmanagedDllFromPath(string unmanagedDllPath) { throw null; }
+ public static void SetAssemblyLocationOverride(System.Func locationOverride) { }
public void SetProfileOptimizationRoot(string directoryPath) { }
public void StartProfileOptimization(string? profile) { }
public override string ToString() { throw null; }
diff --git a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs
index 19604e0455aa74..43f71503442561 100644
--- a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs
+++ b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs
@@ -418,6 +418,63 @@ public static void LoadFromAssemblyPath_DefaultAlc_NonTpaAssembly_MvidMismatch()
}).Dispose();
}
+ [Fact]
+ public static void SetAssemblyLocationOverride_NullArgument_Throws()
+ {
+ // The null check happens before any global state is mutated, so this is safe to run in-process.
+ AssertExtensions.Throws("locationOverride", () => AssemblyLoadContext.SetAssemblyLocationOverride(null));
+ }
+
+ [ConditionalFact(typeof(AssemblyLoadContextTest), nameof(IsRemoteExecutorSupportedAndAssemblyLoadingSupported))]
+ public static void SetAssemblyLocationOverride_OverridesLocationForStreamLoadedAssembly()
+ {
+ // The override is process-wide and set-once, so it must run in its own process.
+ RemoteExecutor.Invoke(static () =>
+ {
+ // Override the location only for assemblies the runtime reports no location for (e.g. loaded
+ // from a stream / memory). Embed the assembly name so we also verify the Assembly argument.
+ AssemblyLoadContext.SetAssemblyLocationOverride(
+ static (assembly, location) => string.IsNullOrEmpty(location) ? $"/overridden/{assembly.GetName().Name}.dll" : location);
+
+ string asmPath = ExtractEmbeddedAssembly("System.Runtime.Loader.Tests.AssemblyVersion1");
+ try
+ {
+ // Loading from a stream means the runtime has no location, so the override kicks in.
+ var streamAlc = new AssemblyLoadContext("LocationOverride_Stream", isCollectible: true);
+ Assembly streamLoaded;
+ using (FileStream fs = File.OpenRead(asmPath))
+ {
+ streamLoaded = streamAlc.LoadFromStream(fs);
+ }
+ Assert.Equal($"/overridden/{streamLoaded.GetName().Name}.dll", streamLoaded.Location);
+
+ // The same assembly loaded from a path (into a separate context) has a real location,
+ // so the callback leaves it untouched.
+ var pathAlc = new AssemblyLoadContext("LocationOverride_Path", isCollectible: true);
+ Assembly fileLoaded = pathAlc.LoadFromAssemblyPath(asmPath);
+ Assert.False(string.IsNullOrEmpty(fileLoaded.Location));
+ Assert.DoesNotContain("/overridden/", fileLoaded.Location);
+ }
+ finally
+ {
+ try { File.Delete(asmPath); } catch { }
+ }
+ }).Dispose();
+ }
+
+ [ConditionalFact(typeof(AssemblyLoadContextTest), nameof(IsRemoteExecutorSupportedAndAssemblyLoadingSupported))]
+ public static void SetAssemblyLocationOverride_CalledTwice_Throws()
+ {
+ RemoteExecutor.Invoke(static () =>
+ {
+ AssemblyLoadContext.SetAssemblyLocationOverride(static (assembly, location) => location);
+ Assert.Throws(
+ () => AssemblyLoadContext.SetAssemblyLocationOverride(static (assembly, location) => location));
+ }).Dispose();
+ }
+
+ private static bool IsRemoteExecutorSupportedAndAssemblyLoadingSupported => RemoteExecutor.IsSupported && PlatformDetection.IsAssemblyLoadingSupported;
+
private static bool IsRemoteExecutorSupportedAndCoreCLR => RemoteExecutor.IsSupported && PlatformDetection.IsAssemblyLoadingSupported && PlatformDetection.IsCoreCLR;
private static string ExtractEmbeddedAssembly(string name)
diff --git a/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs b/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs
index 0e2b66efc1a85c..cc784fa524fcf7 100644
--- a/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs
+++ b/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs
@@ -131,7 +131,7 @@ public override Module ManifestModule
public override string ImageRuntimeVersion => GetInfo(AssemblyInfoKind.ImageRuntimeVersion)!;
- public override string Location => GetInfo(AssemblyInfoKind.Location)!;
+ public override string Location => AssemblyLoadContext.ResolveAssemblyLocation(this, GetInfo(AssemblyInfoKind.Location)!);
// TODO: consider a dedicated icall instead
public override bool IsCollectible => AssemblyLoadContext.GetLoadContext((Assembly)this)!.IsCollectible;