Open vitek-karas opened 5 years ago
@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.
I am currently struggling with what I assume is this issue. I have a .NET 5.0 WPF plugin architecture and encounter errors if I have multiple plugins in different assembly load contexts. It seems to come down to issues with the XAML parser resolving URIs (I get odd control behavior, and "The component 'XXX' does not have a resource identified by the URI" errors.
Given that this is still an open issue, is there any known way to work around the problem? Thanks.
Have you solved your problem?
Have you solved your problem?
No, I have not.
It seems as if there are some serious limitations to building a plug-in architecture with WPF/Core. As described above, it all appears to stem from the XAML parser's ignorance of assembly contexts. Here is a summary of our findings and resolutions.
In order to come up with a workable solution, we did the following:
I finally found a source where my issue has been described. Thanks to dt200r and vitek-karas, I have now a clue what is going wrong. It make sense. I have an assembly with a page and load the assembly two times (at start up and later in my code for reflection). Thanks for sharing all your thoughts on that topic.
Curious why this is classed as a enhancement request. It looks like a regression between .NET Framework and .NET core which suggest it should be classified as a bug.
This issue should get more attention, since it makes the plugin architecture in .NET (Core) unreliable when using WPF for providing UIs and though should be treated as bug rather than as an enhancement.
For our case it helps to set different versions of assemblies.
For example we had different assemblies controls.dll
and they both had version 1.0.0.0 and they both referenced from WPF window. First loaded into default AssemblyLoadContext
and second one loaded to different context. When application tries to create that WPF window using default AssemblyLoadContext
and it resolves to controls.dll
that loaded into non-default AssemblyLoadContext
and then everything is crashing because of incompatible types.
Then I tried to increase version of first controls.dll
and change reference clr-namespace:controls;assembly=controls
to clr-namespace:controls;assembly=controls, Version=2.0.0.0
and it works fine. Unfortunately we have thousands of such references and changing them will be quite a big pain.
But then I found that during XAML compilation it stores version of assembly for references. For for reference clr-namespace:controls;assembly=controls
it will be stored as clr-namespace:controls;assembly=controls, Version=2.0.0.0
in BAML.
Of course it will not solve original problem but perhaps it can help in some cases.
this issue is critical to develop addins as it allows developers to create isolated dependency for the addins to avoid conflicts to other addins and this issue since 2019 . is there any plan when it gets fixed ?
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 theDefault
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 ofAssemblyLoadContext
. 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#L133Assembly 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