Problem
.NET Core 3.0 is trying to enable a simple isolated plugin loading model.
The issue is that the existing reflection API surface changes behavior depending on how the plugin dependencies are loaded. For the problematic APIs, the location of the Assembly directly calling the reflection API, is used to infer the AssemblyLoadContext for reflection loads.
Consider the following set of dependencies:
Assembly pluginLoader; // Assume loaded in AssemblyLoadContext.Default
Assembly plugin; // Assume loaded in custom AssemblyLoadContext
Assembly pluginDependency; // Behavior of plugin changes depending on where this is loaded.
Assembly framework; // Required loaded in AssemblyLoadContext.Default
The .NET Core isolation model allows pluginDependency to be loaded into three distinct places in order to satisfy the dependency of plugin:
AssemblyLoadContext.Default
- Same custom
AssemblyLoadContext as plugin
- Different custom
AssemblyLoadContext as plugin (unusual, but allowed)
Using pluginDependency to determine the AssemblyLoadContext used for loading leads to inconsistent behavior. The plugin expects pluginDependency to execute code on its behalf. Therefore it reasonably expects pluginDependency to use plugin's AssemblyLoadContext. It leads to unexpected behavior except when loaded in the "Same custom AssemblyLoadContext as plugin."
Failing Scenarios
Xunit story
We have been working on building a test harness in Xunit for running the CoreFX test suite inside AssemblyLoadContexts (each test case in its own context). This has proven to be somewhat difficult due to Xunit being a very reflection heavy codebase with tons of instances of types, assemblies, etc. being converted to strings and then fed through Activator. One of the main learnings is that it is not always obvious what will stay inside the “bounds” of an AssemblyLoadContext and what won’t. The basic rule of thumb is that any Assembly.Load() will result in the assembly being loaded onto the AssemblyLoadContext of the calling code, so if code loaded by an ALC calls Assembly.Load(...), the resulting assembly will be within the “bounds” of the ALC. This unfortunately breaks down in some cases, specifically when code calls Activator which lives in System.Private.CoreLib which is always shared.
System.Xaml
This problem also manifests when using an Object deserialization framework which allows specifying assembly qualified type names.
We have seen this issue when porting WPF tests to run in a component in an isolation context. These tests are using System.Xaml for deserialization. During deserialization, System.Xaml is using the affected APIs to create object instances using assembly-qualified type names.
Scope of affected APIs
The problem exists whenever a reflection API can trigger a load or bind of an Assembly and the intended AssemblyLoadContext is ambiguous.
Currently affected APIs
These APIs are using the immediate caller to determine the AssemblyLoadContext to use. As shown above the immediate caller is not necessarily the desired context.
These always trigger assembly loads and are always affected:
namespace System
{
public static partial class Activator
{
public static ObjectHandle CreateInstance(string assemblyName, string typeName);
public static ObjectHandle CreateInstance(string assemblyName, string typeName, bool ignoreCase, BindingFlags bindingAttr, Binder binder, object[] args, CultureInfo culture, object[] activationAttributes);
public static ObjectHandle CreateInstance(string assemblyName, string typeName, object[] activationAttributes);
}
}
namespace System.Reflection
{
public abstract partial class Assembly : ICustomAttributeProvider, ISerializable
{
public static Assembly Load(string assemblyString);
public static Assembly Load(AssemblyName assemblyRef);
}
}
These are only affected when they trigger assembly loads. Assembly loads for these occur when typeName includes a assembly-qualified type reference:
namespace System
{
public abstract partial class Type : MemberInfo, IReflect
{
public static Type GetType(string typeName, bool throwOnError, bool ignoreCase);
public static Type GetType(string typeName, bool throwOnError);
public static Type GetType(string typeName);
}
}
Unamiguous APIs related to affected APIs
namespace System
{
public abstract partial class Type : MemberInfo, IReflect
{
public static Type GetType(string typeName, Func<AssemblyName, Assembly> assemblyResolver, Func<Assembly, string, bool, Type> typeResolver);
public static Type GetType(string typeName, Func<AssemblyName, Assembly> assemblyResolver, Func<Assembly, string, bool, Type> typeResolver, bool throwOnError);
public static Type GetType(string typeName, Func<AssemblyName, Assembly> assemblyResolver, Func<Assembly, string, bool, Type> typeResolver, bool throwOnError, bool ignoreCase);
}
}
In this case, assemblyResolver functionally specifies the explicit mechanism to load. This indicates the current assembly's AssmblyLoadContext is not being used. If the assemblyResolver is only serving as a first or last chance resolver, then these would also be in the set of affected APIs.
Should be affected APIs
Issue https://github.com/dotnet/coreclr/issues/22213, discusses scenarios in which various flavors of the API GetType() is not functioning correctly. As part of the analysis and fix of that issue, the set of affected APIs may increase.
Root cause analysis
In .NET Framework, plugin isolation was provided by creating multiple AppDomain instances. .NET Core dropped support for multiple AppDomain instances. Instead we introduced AssemblyLoadContext.
The isolation model for AssemblyLoadContext is very different from AppDomain. One major distinction was the existence of an ambient property AppDomain.CurrentDomain associated with the running code and its dependents. There is no equivalent ambient property for AssemblyLoadContext.
The issue is that the existing reflection API surface design was based on the existence of an ambient AppDomain.CurrentDomain associated with the current isolation environment. The AppDomain.CurrentDomain acted as the Assembly loader. (In .NET Core the loader function is conceptually attached to AssemblyLoadContext.)
Options
There are two main options:
-
Add APIs which allow specifying an explicit callback to load assemblies. Guide customers to avoid using the APIs which just infer assembly loading semantics on their own.
-
Add an ambient property which corresponds to the active AssemblyLoadContext.
We are already pursuing the first option. It is insufficient. For existing code with existing APIs this approach can be problematic.
The second option allows logical the separation of concerns. Code loaded into an isolation context does not really need to be concerned with how it was loaded. It should expect APIs to logically behave in the same way independent of loading.
This proposal is recommending pursuing the second option while continuing to pursue the first.
Proposed Solution
This proposal is for a mechanism for code to explicitly set a specific AssemblyLoadContext as the ActiveForContextSensitiveReflection for a using block and its asynchronous flow of control. Previous context is restored upon exiting the using block. Blocks can be nested.
AssemblyLoadContext.ActiveForContextSensitiveReflection
namespace System.Runtime.Loader
{
public partial class AssemblyLoadContext
{
private static readonly AsyncLocal<AssemblyLoadContext> asyncLocalActiveContext = new AsyncLocal<AssemblyLoadContext>(null);
public static AssemblyLoadContext ActiveForContextSensitiveReflection
{
get { return _asyncLocalActiveContext.Value; }
}
}
}
AssemblyLoadContext.ActiveForContextSensitiveReflection is a static read only property. Its value is changed through the API below.
AssemblyLoadContext.ActiveForContextSensitiveReflection property is an AsyncLocal<T>. This means there is a distinct value which is associated with each asynchronous control flow.
The initial value at application startup is null. The value for a new async block will be inherited from its parent.
When AssemblyLoadContext.ActiveForContextSensitiveReflection != null
When AssemblyLoadContext.ActiveForContextSensitiveReflection != null, ActiveForContextSensitiveReflection will act as the primary AssemblyLoadContext for the affected APIs.
When used in an affected API, the primary, will:
- determine the set of known Assemblies and from which
AssemblyLoadContext each Assembly is loaded.
- get the first chance to
AssemblyLoadContext.Load(...) before falling back to AssemblyLoadContext.Default to try to load from its TPA list.
- fire its
AssemblyLoadContext.Resolving event if the both of the preceding have failed
Key concepts
- Each
AssemblyLoadContext is required to be idempotent. This means when it is asked to load a specific Assembly by name, it must always return the same result. The result would include whether an Assembly load occurred and into which AssemblyLoadContext it was loaded.
- The set of
Assemblies related to an AssemblyLoadContext are not all loaded by the same AssemblyLoadContext. They collaborate. An assembly loaded into one AssemblyLoadContext, can resolve its dependent Assembly references from another AssemblyLoadContext.
- The root framework (
System.Private.Corelib.dll) is required to be loaded into the AssemblyLoadContext.Default. This means all custom AssemblyLoadContext depend on this code to implement fundamental code including the primitive types.
- If an
Assembly has static state, its state will be associated with its load location. Each load location will have its own static state. This can guide and constrain the isolation strategy.
AssemblyLoadContext loads lazily. Loads can be triggered for various reasons. Loads are often triggered as code begins to need the dependent Assembly. Triggers can come from any thread. Code using AssemblyLoadContext does not require external synchronization. Inherently this means that AssemblyLoadContext are required to load in a thread safe way.
When AssemblyLoadContext.ActiveForContextSensitiveReflection == null
The behavior of .NET Core will be unchanged. Specifically, the effective AssemblyLoadContext will continued to be inferred to be the ALC of the current
caller's Assembly.
AssemblyLoadContext.ActivateForContextSensitiveReflection()
The API for setting ActiveForContextSensitiveReflection is intended to be used in a using block.
namespace System.Runtime.Loader
{
public partial class AssemblyLoadContext
{
public IDisposable ActivateForContextSensitiveReflection();
static public IDisposable ActivateForContextSensitiveReflection(Assembly activating);
}
}
Two methods are proposed.
- Activate
this AssemblyLoadContext
- Activate the
AssemblyLoadContext containing Assembly. This also serves as a mechanism to deactivate within a using block (ActivateForContextSensitiveReflection(null)).
Basic Usage
AssemblyLoadContext alc = new AssemblyLoadContext();
using (alc.ActivateForContextSensitiveReflection())
{
// AssemblyLoadContext.ActiveForContextSensitiveReflection == alc
// In this block, alc acts as the primary Assembly loader for context sensitive reflection APIs.
Assembly assembly = Assembly.Load(myPlugin);
}
Maintaining and restoring original behavior
static void Main(string[] args)
{
// On App startup, AssemblyLoadContext.ActiveForContextSensitiveReflection is null
// Behavior prior to .NET Core 3.0 is unchanged
Assembly assembly = Assembly.Load(myPlugin); // Will load into the Default ALC.
}
void SomeCallbackMethod()
{
using (AssemblyLoadContext.ActivateForContextSensitiveReflection(null))
{
// AssemblyLoadContext.ActiveForContextSensitiveReflection is null
// Behavior prior to .NET Core 3.0 is unchanged
Assembly assembly = Assembly.Load(myPlugin); // Will load into the ALC containing SomeMethod().
}
}
Proposed API changes
namespace System.Runtime.Loader
{
public partial class AssemblyLoadContext
{
public static AssemblyLoadContext ActiveForContextSensitiveReflection { get { return _asyncLocalActiveContext.Value; }}
public IDisposable ActivateForContextSensitiveReflection();
static public IDisposable ActivateForContextSensitiveReflection(Assembly activating);
}
}
Design doc
Detailed design doc is still in early review dotnet/coreclr#23335.
Problem
.NET Core 3.0 is trying to enable a simple isolated plugin loading model.
The issue is that the existing reflection API surface changes behavior depending on how the plugin dependencies are loaded. For the problematic APIs, the location of the
Assemblydirectly calling the reflection API, is used to infer theAssemblyLoadContextfor reflection loads.Consider the following set of dependencies:
The .NET Core isolation model allows
pluginDependencyto be loaded into three distinct places in order to satisfy the dependency ofplugin:AssemblyLoadContext.DefaultAssemblyLoadContextaspluginAssemblyLoadContextasplugin(unusual, but allowed)Using
pluginDependencyto determine theAssemblyLoadContextused for loading leads to inconsistent behavior. ThepluginexpectspluginDependencyto execute code on its behalf. Therefore it reasonably expectspluginDependencyto useplugin'sAssemblyLoadContext. It leads to unexpected behavior except when loaded in the "Same customAssemblyLoadContextasplugin."Failing Scenarios
Xunit story
We have been working on building a test harness in Xunit for running the CoreFX test suite inside
AssemblyLoadContexts (each test case in its own context). This has proven to be somewhat difficult due to Xunit being a very reflection heavy codebase with tons of instances of types, assemblies, etc. being converted to strings and then fed throughActivator. One of the main learnings is that it is not always obvious what will stay inside the “bounds” of anAssemblyLoadContextand what won’t. The basic rule of thumb is that anyAssembly.Load()will result in the assembly being loaded onto theAssemblyLoadContextof the calling code, so if code loaded by an ALC callsAssembly.Load(...), the resulting assembly will be within the “bounds” of the ALC. This unfortunately breaks down in some cases, specifically when code callsActivatorwhich lives inSystem.Private.CoreLibwhich is always shared.System.Xaml
This problem also manifests when using an
Objectdeserialization framework which allows specifying assembly qualified type names.We have seen this issue when porting WPF tests to run in a component in an isolation context. These tests are using
System.Xamlfor deserialization. During deserialization,System.Xamlis using the affected APIs to create object instances using assembly-qualified type names.Scope of affected APIs
The problem exists whenever a reflection API can trigger a load or bind of an
Assemblyand the intendedAssemblyLoadContextis ambiguous.Currently affected APIs
These APIs are using the immediate caller to determine the
AssemblyLoadContextto use. As shown above the immediate caller is not necessarily the desired context.These always trigger assembly loads and are always affected:
These are only affected when they trigger assembly loads. Assembly loads for these occur when
typeNameincludes a assembly-qualified type reference:Unamiguous APIs related to affected APIs
In this case,
assemblyResolverfunctionally specifies the explicit mechanism to load. This indicates the current assembly'sAssmblyLoadContextis not being used. If theassemblyResolveris only serving as a first or last chance resolver, then these would also be in the set of affected APIs.Should be affected APIs
Issue https://github.com/dotnet/coreclr/issues/22213, discusses scenarios in which various flavors of the API
GetType()is not functioning correctly. As part of the analysis and fix of that issue, the set of affected APIs may increase.Root cause analysis
In .NET Framework, plugin isolation was provided by creating multiple
AppDomaininstances. .NET Core dropped support for multipleAppDomaininstances. Instead we introducedAssemblyLoadContext.The isolation model for
AssemblyLoadContextis very different fromAppDomain. One major distinction was the existence of an ambient propertyAppDomain.CurrentDomainassociated with the running code and its dependents. There is no equivalent ambient property forAssemblyLoadContext.The issue is that the existing reflection API surface design was based on the existence of an ambient
AppDomain.CurrentDomainassociated with the current isolation environment. TheAppDomain.CurrentDomainacted as theAssemblyloader. (In .NET Core the loader function is conceptually attached toAssemblyLoadContext.)Options
There are two main options:
Add APIs which allow specifying an explicit callback to load assemblies. Guide customers to avoid using the APIs which just infer assembly loading semantics on their own.
Add an ambient property which corresponds to the active
AssemblyLoadContext.We are already pursuing the first option. It is insufficient. For existing code with existing APIs this approach can be problematic.
The second option allows logical the separation of concerns. Code loaded into an isolation context does not really need to be concerned with how it was loaded. It should expect APIs to logically behave in the same way independent of loading.
This proposal is recommending pursuing the second option while continuing to pursue the first.
Proposed Solution
This proposal is for a mechanism for code to explicitly set a specific
AssemblyLoadContextas theActiveForContextSensitiveReflectionfor a using block and its asynchronous flow of control. Previous context is restored upon exiting the using block. Blocks can be nested.AssemblyLoadContext.ActiveForContextSensitiveReflectionAssemblyLoadContext.ActiveForContextSensitiveReflectionis a static read only property. Its value is changed through the API below.AssemblyLoadContext.ActiveForContextSensitiveReflectionproperty is anAsyncLocal<T>. This means there is a distinct value which is associated with each asynchronous control flow.The initial value at application startup is
null. The value for a new async block will be inherited from its parent.When
AssemblyLoadContext.ActiveForContextSensitiveReflection != nullWhen
AssemblyLoadContext.ActiveForContextSensitiveReflection != null,ActiveForContextSensitiveReflectionwill act as the primaryAssemblyLoadContextfor the affected APIs.When used in an affected API, the primary, will:
AssemblyLoadContexteachAssemblyis loaded.AssemblyLoadContext.Load(...)before falling back toAssemblyLoadContext.Defaultto try to load from its TPA list.AssemblyLoadContext.Resolvingevent if the both of the preceding have failedKey concepts
AssemblyLoadContextis required to be idempotent. This means when it is asked to load a specificAssemblyby name, it must always return the same result. The result would include whether anAssemblyload occurred and into whichAssemblyLoadContextit was loaded.Assembliesrelated to anAssemblyLoadContextare not all loaded by the sameAssemblyLoadContext. They collaborate. An assembly loaded into oneAssemblyLoadContext, can resolve its dependentAssemblyreferences from anotherAssemblyLoadContext.System.Private.Corelib.dll) is required to be loaded into theAssemblyLoadContext.Default. This means all customAssemblyLoadContextdepend on this code to implement fundamental code including the primitive types.Assemblyhas static state, its state will be associated with its load location. Each load location will have its own static state. This can guide and constrain the isolation strategy.AssemblyLoadContextloads lazily. Loads can be triggered for various reasons. Loads are often triggered as code begins to need the dependentAssembly. Triggers can come from any thread. Code usingAssemblyLoadContextdoes not require external synchronization. Inherently this means thatAssemblyLoadContextare required to load in a thread safe way.When
AssemblyLoadContext.ActiveForContextSensitiveReflection == nullThe behavior of .NET Core will be unchanged. Specifically, the effective
AssemblyLoadContextwill continued to be inferred to be the ALC of the currentcaller's
Assembly.AssemblyLoadContext.ActivateForContextSensitiveReflection()The API for setting
ActiveForContextSensitiveReflectionis intended to be used in a using block.Two methods are proposed.
thisAssemblyLoadContextAssemblyLoadContextcontainingAssembly. This also serves as a mechanism to deactivate within a using block (ActivateForContextSensitiveReflection(null)).Basic Usage
Maintaining and restoring original behavior
Proposed API changes
Design doc
Detailed design doc is still in early review dotnet/coreclr#23335.