runtime: System.Text.Json 7.0.2 fails to load in a .net6 application.

Description

We are attempting to reference RestSharp 110.2 which references system.text.json 7.0.2. Our application has many integration scenarios (as a full wpf application, headless service, or as a desktop plugin) and targets net6. We do not currently use deps.json files as they caused our application to not find our own project dependencies during our migration to .net6.

When starting our application we try to use a RestSharp class, this attempts to resolve system.text.json which fails with an error that the assembly cannot be loaded. A reason is not given and the inner exception is null.

Using dotnet-trace I see the following:

HasStack="True" ThreadID="19,208" ProcessorNumber="0" ClrInstanceID="6" AssemblyName="System.Text.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" Stage="ApplicationAssemblies" AssemblyLoadContext="Default" Result="MismatchedAssemblyName" ResultAssemblyName="System.Text.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" ResultAssemblyPath="C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.23\System.Text.Json.dll" ErrorMessage="Requested version 7.0.0.0 is incompatible with found version 6.0.0.0" ActivityID="/#15712/1/225/" 

I have verified in the module window in visual studio that no version of system.text.json is loaded at this time.

In a small test project, we can recreate this issue by removing the deps.json file.

Reproduction Steps

please see: https://github.com/pinzart90/SytemTextDemo/tree/main

this test project sets GenerateDependencyFile to false at build time, setting it to true resolves the issue. But that causes other issues for our application and integration.

We’d like to understand if the behavior we are seeing is a bug or as designed and if there are any workarounds.

Expected behavior

We expect that STJ 7.0 will be loaded in net6 since we reference it and no other earlier version of STJ is loaded. We expect that it will be loaded even if a GenerateDependencyFile is false at build time and we do not have deps.json files.

Actual behavior

The assembly fails to load even when using an assembly resolver.

Regression?

unknown

Known Workarounds

Hoping you can provide some.

Configuration

6.0.415 windows 11 pro x64 no na

Other information

No response

About this issue

  • Original URL
  • State: open
  • Created 8 months ago
  • Comments: 15 (9 by maintainers)

Most upvoted comments

Let me start with: Please do not delete .deps.json files. The whole system is designed to work with them. The host supports running the app without it, but it’s really only a backward compatibility behavior. Even if there are weird quirks in that behavior, we are very unlikely to try to fix those.

Specifically for the ability to carry the same assembly in the app as the one in the framework: The functionality is designed to let the app carry the minimum version it needs, but it is expected to upgrade to a newer version if the framework the app is running on has a newer version. For this, the host needs version information which it gets from the .deps.json. More specifically - it is not designed to “always use the one from the app”, that would not work if the app is running on let’s say .NET 8, because other parts of the framework would not be compatible with a 7.0 System.Text.Json.

I’ll try to answer all the questions, let me know if I missed something. In no particular order:

If I delete .deps.json how come the assembly is resolved from the framework and not from the app’s folder.

This is because the host reads version information from the .deps.json file - the host does not read version information from the assembly itself (it would be prohibitively expensive to do that). In the case the app has no .deps.json it has no version information about the file in the app, but it does have version information about the file in the framework (because the framework has .deps.json). In this case it prefers the one with the version information. This is the relevant log from the host when this happens:

Replacing deps entry [repro_app\bin\Debug\net6.0\System.Text.Json.dll, AssemblyVersion:, FileVersion:] with [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.24\System.Text.Json.dll, AssemblyVersion:6.0.0.0, FileVersion:6.0.2423.51814]

Custom resolution logic as the first thing in Main

This should generally work, with the caveat mentioned by Eric above. Assembly resolution happens at JIT time, and JIT will process entire methods at once, before executing them. So, having a reference to the assembly JITed before the resolution handler is activated can happen. Please also note that JIT can inline methods, so just putting the code into a separate method may not solve this.

As for the code in your repro - I would advice against using Assembly.LoadFrom - honestly pretty much ever, but specifically in this case. Your custom resolution logic tries to help the runtime resolve assemblies as if it were resolved by the runtime itself. For that please use AssemblyLoadContext.Default.LoadFromAssemblyPath - that is as close as you can get to the behavior the runtime has when resolving the assembly on its own. Assembly.LoadFrom tries to implement the behavior ii had in .NET Framework which requires additional complexity, one of it being that there will be another handler for the AssemblyResolve event registered by the runtime, and the ordering might get “interesting”.

Why using a custom resolution handler doesn’t help with the System.Text.Json problem

The runtime prefers the TPA over other assemblies, specifically the version. If you try to load an assembly by hand which is also in the TPA, the runtime will actually compare the assembly version of the one you’re trying to load to the one in the TPA (in your case the former is 7.0.0.0 and the latter is 6.0.0.0). The load will fail if they don’t match.

Our app also has around 50 assemblies. Do we need deps.json files for all of them.

You should not even get .deps.json for anything but the main app. By default SDK doesn’t copy .deps.json from dependency projects into the app - you should not need to do that either. The host will only consider the .deps.json for the app (so if the app is called MyApp.dll, then it will look at MyApp.deps.json) and then .deps.json files from the frameworks the app depends on (so Microsoft.NETCore.App.deps.json and potentially also ASP.NET or WindowsDesktop if your app uses those). Any other .deps.json files in the directory will be ignored.

How does this work for plugins

That depends on the application which hosts the plugins (to avoid confusion I’ll call this the plugin-host here). If the plugin-host implements plugin isolation, it will typically do so via a new AssemblyLoadContext and in that case it may (and typically will) choose to use AssemblyDependencyResolver which will then try to read the .deps.json for the plugin. So if my main plugin assembly is MyPlugin.dll, then the AssemblyDependencyResolver will look for MyPlugin.deps.json. The processing there will be basically the same as if the plugin is an app - that is it will read the MyPlugin.deps.json and then also consider any frameworks. It will NOT consider the plugin-host’s .deps.json - intentionally. It is up to the plugin-host to decide the resolution strategy for assemblies which are present in the plugin-host as well as the plugin. As Eric mentioned above, it will typically try to unify the ones used in the interface between the plugin-host and the plugin, and then let the rest be loaded per plugin’s request (isolated). But plugin-hosts may choose a different strategy. The assembly resolution and what it needs from the plugin should be part of the plugin-host/plugin contract description to make it clear.

This is a better question for @agocke, @vitek-karas, @jkotas and others on the loader team.

You can check out the docs here: https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/understanding-assemblyloadcontext

do we need deps.json files for all of them. Or just the entrypoint assembly

The deps file is needed for each “load context” - think of that as a cohesive graph of dependencies like a single application or single plugin.

When you have an application + plugins you probably want to have a deps file for the application and its dependencies, and allow for a deps file for plugins - so they are free to load their own dependencies without affecting the host or other plugins. The host will need to make sure that any exchange types between the host/plugin are unified. A plugin needs to agree with the host on all the assemblies that make up the host/plugin interface - and ideally those are minimal to allow for plugins to update components like System.Text.Json, Microsoft.Extensions.*, etc without breaking the host/plugin interface. I believe that’s covered the docs linked above.

The load contexts (AssemblyLoadContext or ALC for short) do not form hierarchy. If you return null from AssemblyLoadContext.Load it falls back always to the Default ALC.

That said, it’s your choice to do so. You can just as easily call the AssemblyLoadContext.LoadFromAssemblyName on the ALC in which the HybridApp is loaded (the ALC for the PluginApp is implemented in the HybridApp, so it can decide to forward that call to the ALC of itself - for example AssemblyLoadContext.GetLoadContext(typeof(PluginLoadContext).Assembly).LoadFromAssemblyName(asmName).

I should note that this will work regardless if the HybridApp is executed standalone or as a plugin.

Looks like the problem with using deps file was that we were using CopyLocal=False in most of our project dependencies. Switching it to CopyLocal=true allows our program to startup correctly with the deps files on disk. This also seems to allow us to force the version of System.Text.Json (vers 7) by adding it as a simple nuget package reference in one of our projects.

@ericstj our application is used as an addin (dynamically loaded as part of another host app). Our app also has around 50 assemblies. Do we need deps.json files for all of them. Or just the entrypoint assembly ? We want to make sure that a similar issue does not happen for other assemblies (other framework assemblies, or host app assemblies).

I am also not clear on the load order. Which deps file is used to resolve an assembly reference? Is it the “closest” deps file to the assembly that has the reference ? Something like AddingApp deps.json => HostApp deps.json => Dotnet Framework deps.json

I’m not the best person to answer your question but I think @ericstj or someone from the host team should know, cc @agocke