Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 6a48f05

Browse files
authored
Add ALC.ContextualReflection.md (#23335)
1 parent cbeadac commit 6a48f05

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# AssemblyLoadContext.CurrentContextualReflectionContext
2+
## Problem
3+
.NET Core 3.0 is trying to enable a simple isolated plugin loading model.
4+
5+
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.
6+
7+
Consider the following set of dependencies:
8+
```C#
9+
Assembly pluginLoader; // Assume loaded in AssemblyLoadContext.Default
10+
Assembly plugin; // Assume loaded in custom AssemblyLoadContext
11+
Assembly pluginDependency; // Behavior of plugin changes depending on where this is loaded.
12+
Assembly framework; // Required loaded in AssemblyLoadContext.Default
13+
```
14+
15+
The .NET Core isolation model allows `pluginDependency` to be loaded into three distinct places in order to satisfy the dependency of `plugin`:
16+
* `AssemblyLoadContext.Default`
17+
* Same custom `AssemblyLoadContext` as `plugin`
18+
* Different custom `AssemblyLoadContext` as `plugin` (unusual, but allowed)
19+
20+
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`."
21+
22+
### Failing Scenarios
23+
#### Xunit story
24+
25+
We have been working on building a test harness in Xunit for running the CoreFX test suite inside `AssemblyLoadContext`s (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.
26+
27+
#### System.Xaml
28+
This problem also manifests when using an `Object` deserialization framework which allows specifying assembly qualified type names.
29+
30+
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.
31+
32+
### Scope of affected APIs
33+
The problem exists whenever a reflection API can trigger a load or bind of an `Assembly` and the intended `AssemblyLoadContext` is ambiguous.
34+
#### Currently affected APIs
35+
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.
36+
37+
These always trigger assembly loads and are always affected:
38+
```C#
39+
namespace System
40+
{
41+
public static partial class Activator
42+
{
43+
public static ObjectHandle CreateInstance(string assemblyName, string typeName);
44+
public static ObjectHandle CreateInstance(string assemblyName, string typeName, bool ignoreCase, BindingFlags bindingAttr, Binder binder, object[] args, CultureInfo culture, object[] activationAttributes);
45+
public static ObjectHandle CreateInstance(string assemblyName, string typeName, object[] activationAttributes);
46+
}
47+
}
48+
namespace System.Reflection
49+
{
50+
public abstract partial class Assembly : ICustomAttributeProvider, ISerializable
51+
{
52+
public static Assembly Load(string assemblyString);
53+
public static Assembly Load(AssemblyName assemblyRef);
54+
}
55+
}
56+
```
57+
These are only affected when they trigger assembly loads. Assembly loads for these occur when `typeName` includes a assembly-qualified type reference:
58+
```C#
59+
namespace System
60+
{
61+
public abstract partial class Type : MemberInfo, IReflect
62+
{
63+
public static Type GetType(string typeName, bool throwOnError, bool ignoreCase);
64+
public static Type GetType(string typeName, bool throwOnError);
65+
public static Type GetType(string typeName);
66+
}
67+
}
68+
```
69+
#### Unamiguous APIs related to affected APIs
70+
```C#
71+
namespace System
72+
{
73+
public abstract partial class Type : MemberInfo, IReflect
74+
{
75+
public static Type GetType(string typeName, Func<AssemblyName, Assembly> assemblyResolver, Func<Assembly, string, bool, Type> typeResolver);
76+
public static Type GetType(string typeName, Func<AssemblyName, Assembly> assemblyResolver, Func<Assembly, string, bool, Type> typeResolver, bool throwOnError);
77+
public static Type GetType(string typeName, Func<AssemblyName, Assembly> assemblyResolver, Func<Assembly, string, bool, Type> typeResolver, bool throwOnError, bool ignoreCase);
78+
}
79+
}
80+
```
81+
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.
82+
#### Should be affected APIs
83+
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.
84+
### Root cause analysis
85+
In .NET Framework, plugin isolation was provided by creating multiple `AppDomain` instances. .NET Core dropped support for multiple `AppDomain` instances. Instead we introduced `AssemblyLoadContext`.
86+
87+
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`.
88+
89+
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`.)
90+
91+
## Options
92+
93+
There are two main options:
94+
95+
1. 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.
96+
97+
1. Add an ambient property which corresponds to the active `AssemblyLoadContext`.
98+
99+
We are already pursuing the first option. It is insufficient. For existing code with existing APIs this approach can be problematic.
100+
101+
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.
102+
103+
This proposal is recommending pursuing the second option while continuing to pursue the first.
104+
105+
## Proposed Solution
106+
This proposal is for a mechanism for code to explicitly set a specific `AssemblyLoadContext` as the `CurrentContextualReflectionContext` for a using block and its asynchronous flow of control. Previous context is restored upon exiting the using block. Blocks can be nested.
107+
108+
### `AssemblyLoadContext.CurrentContextualReflectionContext`
109+
110+
```C#
111+
namespace System.Runtime.Loader
112+
{
113+
public partial class AssemblyLoadContext
114+
{
115+
private static readonly AsyncLocal<AssemblyLoadContext> asyncLocalActiveContext = new AsyncLocal<AssemblyLoadContext>(null);
116+
public static AssemblyLoadContext CurrentContextualReflectionContext
117+
{
118+
get { return _asyncLocalCurrentContextualReflectionContext.Value; }
119+
}
120+
}
121+
}
122+
```
123+
`AssemblyLoadContext.CurrentContextualReflectionContext` is a static read only property. Its value is changed through the API below.
124+
125+
`AssemblyLoadContext.CurrentContextualReflectionContext` property is an `AsyncLocal<T>`. This means there is a distinct value which is associated with each asynchronous control flow.
126+
127+
The initial value at application startup is `null`. The value for a new async block will be inherited from its parent.
128+
129+
#### When `AssemblyLoadContext.CurrentContextualReflectionContext != null`
130+
131+
When `AssemblyLoadContext.CurrentContextualReflectionContext != null`, `CurrentContextualReflectionContext` will act as the primary `AssemblyLoadContext` for the affected APIs.
132+
When used in an affected API, the primary, will:
133+
* determine the set of known Assemblies and how to load the Assemblies.
134+
* get the first chance to `AssemblyLoadContext.Load(...)` before falling back to `AssemblyLoadContext.Default` to try to load from its TPA list.
135+
* fire its `AssemblyLoadContext.Resolving` event if the both of the preceding have failed
136+
137+
##### Key concepts
138+
139+
* 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.
140+
* 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`.
141+
* 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.
142+
* 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.
143+
* `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.
144+
145+
#### When `AssemblyLoadContext.CurrentContextualReflectionContext == null`
146+
147+
The behavior of .NET Core will be unchanged. Specifically, the effective `AssemblyLoadContext` will continued to be inferred to be the ALC of the current
148+
caller's `Assembly`.
149+
150+
### `AssemblyLoadContext.EnterContextualReflection()`
151+
152+
The API for setting `CurrentContextualReflectionContext` is intended to be used in a using block.
153+
154+
```C#
155+
namespace System.Runtime.Loader
156+
{
157+
public partial class AssemblyLoadContext
158+
{
159+
public ContextualReflectionScope EnterContextualReflection();
160+
161+
static public ContextualReflectionScope EnterContextualReflection(Assembly activating);
162+
}
163+
}
164+
```
165+
166+
Two methods are proposed.
167+
1. Activate `this` `AssemblyLoadContext`
168+
2. Activate the `AssemblyLoadContext` containing `Assembly`. This also serves as a mechanism to deactivate within a using block (`EnterContextualReflection(null)`).
169+
170+
#### Basic Usage
171+
```C#
172+
AssemblyLoadContext alc = new AssemblyLoadContext();
173+
using (alc.EnterContextualReflection())
174+
{
175+
// AssemblyLoadContext.CurrentContextualReflectionContext == alc
176+
// In this block, alc acts as the primary Assembly loader for context sensitive reflection APIs.
177+
Assembly assembly = Assembly.Load(myPlugin);
178+
}
179+
```
180+
#### Maintaining and restoring original behavior
181+
```C#
182+
static void Main(string[] args)
183+
{
184+
// On App startup, AssemblyLoadContext.CurrentContextualReflectionContext is null
185+
// Behavior prior to .NET Core 3.0 is unchanged
186+
Assembly assembly = Assembly.Load(myPlugin); // Will load into the Default ALC.
187+
}
188+
189+
void SomeCallbackMethod()
190+
{
191+
using (AssemblyLoadContext.EnterContextualReflection(null))
192+
{
193+
// AssemblyLoadContext.CurrentContextualReflectionContext is null
194+
// Behavior prior to .NET Core 3.0 is unchanged
195+
Assembly assembly = Assembly.Load(myPlugin); // Will load into the ALC containing SomeMethod().
196+
}
197+
}
198+
```
199+
## Approved API changes
200+
201+
```C#
202+
namespace System.Runtime.Loader
203+
{
204+
public partial class AssemblyLoadContext
205+
{
206+
public static AssemblyLoadContext CurrentContextualReflectionContext { get { return _asyncLocalCurrentContextualReflectionContext.Value; }}
207+
208+
public ContextualReflectionScope EnterContextualReflection();
209+
210+
static public ContextualReflectionScope EnterContextualReflection(Assembly activating);
211+
212+
[EditorBrowsable(EditorBrowsableState.Never)]
213+
public struct ContextualReflectionScope : IDisposable
214+
{
215+
}
216+
}
217+
}
218+
```
219+
## Design doc
220+
221+
Mostly TBD.
222+
223+
### Performance Consideration
224+
#### Avoiding native / managed transitions
225+
226+
My natural inclination would be to replace most native calls taking a `StackCrawlMark` with callS taking the `CurrentContextualReflectionContext`. When `CurrentContextualReflectionContext` is `null`, resolve the `StackCrawlMark` first, passing the result as the inferred context.
227+
228+
However this may require mutiple native/managed transitions. Performance considerations may require Native calls which currently take a `StackCrawlMark` will need to be modified to also take `CurrentContextualReflectionContext`.
229+
230+
### Hypothetical Advanced/Problematic use cases
231+
232+
One could imagine complicated scenarios in which we need to handle ALCs on event callback boundaries. These are expected to be rare. The following are representative patterns that demonstrate the possibility to be support these more complicated usages.
233+
234+
#### An incoming event handler into an AssemblyLoadContext
235+
```C#
236+
void OnEvent()
237+
{
238+
using (alc.Activate())
239+
{
240+
...
241+
}
242+
}
243+
```
244+
#### An incoming event handler into a collectible AssemblyLoadContext
245+
```C#
246+
class WeakAssemblyLoadContextEventHandler
247+
{
248+
WeakReference<AssemblyLoadContext> weakAlc;
249+
250+
void OnEvent()
251+
{
252+
AssemblyLoadContext alc;
253+
if(weakAlc.TryGetTarget(out alc))
254+
{
255+
using (alc.Activate())
256+
{
257+
...
258+
}
259+
}
260+
}
261+
}
262+
```
263+
#### A outgoing callback
264+
```C#
265+
using (AssemblyLoadContext.Activate(null))
266+
{
267+
Callback();
268+
}
269+
```
270+
#### An outgoing event handler
271+
```C#
272+
void OnEvent()
273+
{
274+
using (AssemblyLoadContext.Activate(null))
275+
{
276+
...
277+
}
278+
}
279+
```

0 commit comments

Comments
 (0)