Trying to load XAML into non-default AssemblyLoadContext can lead to very confusing errors. The underlying problem is that XAML parser is not aware of assembly load contexts. The parser itself typically runs in the Default load context, but it may be triggered to load XAML into a custom (secondary) load context. In that case all assembly resolution should happen via the secondary load context.
In order to make this easier .NET Core 3.0 introduced the "contextual reflection" concept, which can switch all reflection based APIs to use the secondary load context. More details about contextual reflection can be found in AssemblyLoadContext.ContextualReflection.md.
That fixes all cases where the XAML parser uses reflection APIs like Assembly.GetType and similar. Unfortunately the XAML parser implements its own assembly resolution logic in some cases. This logic was copied from the .NET Framework version of WPF and it still relies AppDomains, GAC and so on - it is not aware of AssemblyLoadContext. This logic can break the correct behavior: Among other things it walks all assemblies loaded into the current AppDomain (so all assemblies in the process, as .NET Core has only one AppDomain) and using custom logic resolves assembly against that list. If there are two assemblies of the same (or similar) names in that list, it will basically randomly pick the first one it finds. The code which does that is here: https://github.com/dotnet/wpf/blob/ac9d1b7a6b0ee7c44fd2875a1174b820b3940619/src/Microsoft.DotNet.Wpf/src/Shared/MS/Internal/SafeSecurityHelper.cs#L133
Assembly load contexts are typically used to implement plugin architecture. To provide good levels of isolation for each plugin, every plugin is loaded into its own load context. This can very easily lead to cases where each plugin has its own version of a certain dependency. But the above mentioned code ignores the isolation of load contexts, and will resolve assembly globally - leading to cases where the plugins will get the wrong version of dependency used.
A sample repro app is here: https://github.com/vitek-karas/WPFPluginLoadProblem
This app shows the problem with a typical plugin architecture (host app loading two plugins, each using XAML parser to load some XAML).
Originally this problem was found trying to implement tests on WPF, using ALC to provide isolation of WPF itself. The repro of that case is here: https://github.com/nick-beer/ALC-XAML-LOAD-BUG
This repro boils down to the same underlying problem.
/cc @nick-beer
@vitek-karas Reading through the docs etc., I'm trying to figured out the best way to enumerate loaded assemblies in an AssemblyLoadContext.
We don't want to load an Assembly if it's not already loaded for other reasons - we just want to find out if an Assembly is already loaded, and if it is, then work with it.
I'm trying to figured out the best way to enumerate loaded assemblies in an AssemblyLoadContext.
Whats wrong with AssemblyLoadContext.Assemblies ? doc says "Returns a collection of the Assembly instances loaded in the AssemblyLoadContext."
we just want to find out if an Assembly is already loaded, and if it is, then work with it
The key is probably to use CurrentContextualReflectionContext or provide some other way the caller can communicate which AssemblyLoadContext to use, otherwise you would just replicate the current behavior.
I absolutely agree with @weltkante here - the key part of the fix should be to use the right AssemblyLoadContext. How exactly is that determined depends on the actual use case, but given that we're unlikely to change the public api of the parser, using the CurrentContextualReflectionContext is probably the right way to go. Alternatively you could use just Assembly.Load which will use it.
As for enumerating assemblies, I'm really wondering why do you need to do that. As mentioned above the primary goal is to load the assembly through the right ALC. Once you have the right ALC you can just call AssemblyLoadContext.LoadFromAssemblyName. The runtime has a lookup cache very early on in this code path, so it should not be necessary for you to cache the result.
That said I noticed that the current code in WPF tries to implement somewhat custom "assembly name" -> "assembly" resolution - for example it sometimes intentionally ignores versions and so on. If we need to keep that behavior, then you would probably need to rely on assembly enumeration, in which case the Assemblies collection is the right API. This API is relatively slow, so in general I would suggest to not use it unless necessary.
Personally I'm curious why does WPF need to implement all of this custom assembly loading logic - why not use simple Assembly.Load.
Future looking: We recommend to not rely on the contextual reflection APIs, exactly because of the problem described in this issue. Almost all reflection APIs already have overloads which have some way to explicitly specify which ALC to use. For example Type.GetType has an overload which takes assembly resolver callback.
Adding something similar to the XAML parser API would be very nice.
why not use simple Assembly.Load
Probably because it requires a fully qualified name including version and public key, and you don't have to specify fully qualified names in XAML. In fact most users probably don't use them (I certainly never wrote nor have seen a fully qualified assembly name in XAML ever). So removing that capability will be a breaking change to existing XAML.
Assembly.Load does not require fully qualified names. It accepts whatever the constructor of AssemblyName accepts, which can handle partial assembly names just fine. I tried with a simple case like this:
Loading 'System.Xml'
Success: System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 at C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0-preview8-28379-12\System.Xml.dll
Loading 'System.Xml, Version=4.0.0.0'
Success: System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 at C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0-preview8-28379-12\System.Xml.dll
In fact in .NET Core Assembly.Load(string) and Assembly.LoadWithPartialName(string) are the same (they have slightly different argument checking, but otherwise they end up calling the exact same implementation passing the name as-is).
Thanks for the link describing how WPF behaves. Roughly speaking Assembly.Load should do what WPF describes in step 1, but the devil will be in the details - it's likely that the two algorithms will not match exactly. In that case the assembly enumeration is probably the only way to go.
This is probably historical, on Desktop Framework Assembly.Load didn't work unless you fully qualified the name (I keep having to work around this in our applications scripting engine regularly).
For the sake of compatibility I doubt they can move away from the established loading behavior, in particular how versions are treated in presence of loaded assemblies could differ.
If this is really a major performance problem in .NET Core they could consider switching to Assembly.Load for .NET Core 5 (or some other major version) and providing a compatibility switch to get old loading semantics. But any such optimizations should happen separately after fixing the AssemblyLoadContext problem of this issue.
I absolutely agree that solving the ALC problem should come first.
Most helpful comment
Whats wrong with AssemblyLoadContext.Assemblies ? doc says "Returns a collection of the Assembly instances loaded in the AssemblyLoadContext."
The key is probably to use CurrentContextualReflectionContext or provide some other way the caller can communicate which AssemblyLoadContext to use, otherwise you would just replicate the current behavior.