-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dependencies isolation bug in AssemblyLoadContext #87578
Comments
Tagging subscribers to this area: @vitek-karas, @agocke, @VSadov Issue DetailsDescriptionIf you meet the following conditions:
Then the dependency isolation of dynamically loaded modules is broken. It contains TWO references to package X in its context of loaded assemblies. Thus, further work with this package is impossible, because we will get exceptions like "A cannot be cast to A" at any attempt to access the package's functionality. The text of the exception may change depending on the conditions (e.g., to "method does not exist"), but the meaning remains the same. This seems to be a common enough case that it should be fixed immediately. Reproduction StepsI have created a minimal project that reproduces the problem:
As you can notice, after building and running the project you will get an exception of the "A cannot be cast to A" kind. However, you only need to remove the reference to the third-party nuget package from - <PackageReference Include="Csla" Version="7.0.0" /> And also by removing using to it from - using Csla; Then everything will definitely work as expected (of course, do NOT forget to restore nuget packages, clean up the Expected behaviorProgram output:
Actual behaviorThe throwing out of the exception:
Regression?This problem seems to have existed since the development of Known WorkaroundsAs a temporary solution, you can track in // We can also compare the version, etc.
if(assemblyName.Name = "Microsoft.Extensions.Logging.Abstractions")
{
return null; // Skip loading.
} Configuration.NET Version: 7.0 The same problem in Linux/Windows, so the current configuration is not so important. Other informationNo response
|
This is very common and if dependencies bleed from one context into the other then you will end up with problems. When using load contexts, dependencies that cross the boundary between contexts need to be unified. So, in your example project, plugins cannot load their own version of the logging abstractions since that dependency is already going to be provided by the host. If you're just trying to load assemblies dynamically into the main process there's no need for a custom load context, just use the default one. Otherwise, you should think about any plugin system like this:
Since you're using the DI container, I assume dependencies added to it from the host, will flow into the plugins. You need to account for that when determining where to load assemblies from (the plugin or host). In the image below, any type passed to Plugin A (in your example ILogger) from the host needs to come from the same place. In your example: Your plugin is using Microsoft.Extensions.Loggging from Csla in the pluginloadcontext and the host is using Microsoft.Extensions.Loggging from the default load context. |
@davidfowl, thank you very much for your reply! I really appreciate it. |
Private dependencies can truly be private, but then you can't use the DI container to provide them which wants to share dependencies. You don't need to control what dependencies other packages used, but you need to unify (aka move them to shared) any dependencies that are loaded or reference by the host. You need to have a list of assemblies that are loaded by the host and then you need to add logic here: To detect if one of those assemblies are being loaded, then you need to delegate to |
@davidfowl, so, let's commit the solution for "descendants" (those who will read this issue). Am I correct in my understanding that in general, if the requirements specified by you are met, a check like this (or some similar logic) in protected override Assembly Load(AssemblyName assemblyName)
{
// Check if such the assembly has already been loaded by Host.
if (AssemblyLoadContext.Default.Assemblies
.FirstOrDefault(x => x.FullName == assemblyName.FullName) is not null)
{
return null;
}
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
} This helped in my project and now everything works. However, I want to make sure that I solved the problem adequately, without using "smelly code", etc. |
Unfortunately this is not a reliable way to fix this problem. The The correct way to do this is to write something like: try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyName(name);
if (asm != null)
{
return asm;
}
}
catch
{
// Assembly is not part of the host - load it into the plugin
}
// Load the dependency from the plugin There's some more discussion around the same topic here: #87185 Just to expand on David's response. For the plugin isolation to work correctly, any types involved in the contract between the host and plugin must NOT be isolated, instead, those types should always be provided by the host. In simple cases the contract is the interface the plugin implements, and the host uses to call into the plugin, and so it's relatively easy to reason about what is in it. But shared global state is also part of the contract, even though it's not explicitly visible. DI is an example of a shared global state (the service registrations are the shared state) and so it effectively extends the contract to include all of the types used in the DI (or at least the subset used or provided by the plugin). One final point (since we've had a long discussion on this recently internally): The usage of |
@vitek-karas, thank you so much for your reply! Everything seems to be falling into place now. I think we can now close the current issue. |
@vitek-karas we should write this up somewhere with a better diagram than mine 😄 |
@davidfowl, guys, that's a really good idea. Because before I created the current issue, I literally searched all over the Internet trying to find an adequate solution to my problem. Creating this issue was a last resort. |
Description
If you meet the following conditions:
AssemblyLoadContext
to dynamically load modules (.dll);X
;X
(i.e. the same one used in step 2).Then the dependency isolation of dynamically loaded modules is broken. It contains TWO references to package X in its context of loaded assemblies. Thus, further work with this package is impossible, because we will get exceptions like "A cannot be cast to A" at any attempt to access the package's functionality. The text of the exception may change depending on the conditions (e.g., to "method does not exist"), but the meaning remains the same.
This seems to be a common enough case that it should be fixed immediately.
Reproduction Steps
I have created a minimal project that reproduces the problem:
https://github.com/hyperion-cs/net-assemblyloadcontext-isolation-problem
Csla
version 7.0.0 is used as the problematic third-party nuget package. However, this is not crucial. ANY nuget package that has internal dependencies matching the parent (EntryPoint
) project can be used in the same way. For example,Telegram.Bots.Extensions.Polling
version 5.9.0 may also suit.As you can notice, after building and running the project you will get an exception of the "A cannot be cast to A" kind. However, you only need to remove the reference to the third-party nuget package from
NET_AssemblyLoadContext_IsolationProblem.Lib.csproj
:- <PackageReference Include="Csla" Version="7.0.0" />
And also by removing using to it from
LibClass.cs
:- using Csla;
Then everything will definitely work as expected (of course, do NOT forget to restore nuget packages, clean up the
libext
folder in theDebug
output folder of theEntryPoint
project (it should not containCsla.dll
, etc.), and rebuild the entire solution). Note that after the build,AfterBuild
is used to copy the extension files.Expected behavior
Program output:
Actual behavior
The throwing out of the exception:
Regression?
This problem seems to have existed since the development of
AssemblyLoadContext
.Known Workarounds
As a temporary solution, you can track in
ProblemLoadContext.Load(...)
the re-loading of problematic dependencies and skip them. But this is a very dirty and bad solution. Example:Configuration
.NET Version: 7.0
OS: macOS 13.4
Architecture: x64
The same problem in Linux/Windows, so the current configuration is not so important.
Other information
No response
The text was updated successfully, but these errors were encountered: