Skip to content

Working version#1

Open
NPatch wants to merge 1 commit intoMarcelVersteeg:masterfrom
NPatch:master
Open

Working version#1
NPatch wants to merge 1 commit intoMarcelVersteeg:masterfrom
NPatch:master

Conversation

@NPatch
Copy link

@NPatch NPatch commented Jan 27, 2026

First off, I added MessageImportance.High in Log.LogMessage calls because otherwise they are not displayed and debugging becomes difficult.

Second, I noticed that in:

NpmPackage? npmPackage = RegistryClient.GetPackageData(npmPackageInfo.Name,
null/*ns*/,
CancellationToken.None).
Result;

RegistryClient.GetPackageData is called and Result is asked for instantly when you're supposed to await it.
Instead store in Task<NpmPackage?> var and call .Wait(). Probably the reason why it's not always working right.

I tried a few things to get it working with the JsonSerializerContext, but ultimately, I opted to add a local version of the NpmRegistry.Wrapper and try out stuff. One such case was to switch GetFromJsonAsync(url, context, ...) to GetFromJsonAsync<NpmPackage>(url,...) and it worked, as in it moved on, did not throw the error and tried to get the json data from the registry. But it did fail to get everything correctly. My theory is that it failed to grab fields that were separate classes (DictTags, Versions etc).

This part specifically failed because DistTags was null, and because of Log.LogError it failed after just chart.js:

string? latestVersionString = npmPackage.DistTags?.Latest;
if (null != latestVersionString)

As for the main issue, I think it's because System.Net.Http.Json, among others, is trimmable (iirc, I saw the it in dnspy) and by using the interface to decouple the Task dll from the NpmRegistryClient code basically hides the json stuff,
which likely ends up hiding the call to the assembly, ergo it's trimmed, which also likely ends up trimming types from the NpmRegistry.Wrapper assembly. One more thing that also nudges me towards the trimming
idea is that NpmRegistry.Wrapper, uses generators for the JsonSerializerContext (much like DataTypes and Tasks) and they are likely not generated and propagated through the assembly properly (at least when integrated through the nuget package).

I looked at the _bin/ folders where other libs have no problem generating their own JsonSerializerContext classes, but NpmRegistry.Wrapper's was nowhere to be found.

So to try and test this, I added a console app to call the Task directly for a first test, removed the ModelsSerializerContext, provided my own version of GetFromJsonAsync, basically a function that downloads the whole response first into a byte array and calls deserialize and it worked. The idea was to remove the dependency to the trimmable function call and also by removing the JsonSerializerContext, remove the problem of the generated files being inaccessible to the Task code on runtime. Got the response for all 4 npm packages, fully deserialized.

@MarcelVersteeg
Copy link
Owner

@NPatch Sorry for the late reply, other more important things came up. I looked at your patch, and I still have a few things that bother me.

Implementing your own deserializer is something that has also crossed my mind. However, by disabling the JsonSerializerContext, the NpmRegistryClient is no longer AOT compatible as far as I know. You also suppressed the IL3050 error I see. The addition of the JsonSerializerContext was actually something I contributed to the package, in an attempt to fix the issue you also encountered, that originally it did not return the complete data (indeed especially DistTags property was null), while the complete JSON was indeed received.

The funny thing is though, that when I use my original version and run the Tasks project as an application (that executes the same task code), it DOES succeed. This makes me wonder if trimming is really an issue here. If it would be, I would expect the same issue to occur when the task is executed directly as a console application. I also tried to run the task during the build after explicitly setting the options IsAotCompatible and PublishTrimmed in the root Directory.Build.props to False and this did not resolve the exception (though the NuGet package is of course still marked as IsAotCompatible). I assume that explicitly setting these properties to False would disable all trimming.

You also mention that the JsonSerializerContext might not be propagated correctly via the NuGet package. What I do see when I open the NpmRegistry.Wrapper.dll (as it is in the downloaded NuGet package) in ILSpy, that the NpmRegistry.Wrapper.Models.ModelsSerializerContext is there and contains all the type information of all the classes in the model, including all the generated deserialization code.

I looked at the _bin/ folders where other libs have no problem generating their own JsonSerializerContext classes, but NpmRegistry.Wrapper's was nowhere to be found.
When building it yourself with the default properties as I have defined, I would indeed expect generated files to be saved in _bin/_application/_generated/NpmRegistry.Wrapper/Debug (for a debug build). It is indeed strange that these generated files are not there.

I really appreciate your suggestions, but so far I am not very happy with the proposed solution..

@NPatch
Copy link
Author

NPatch commented Feb 7, 2026

For what it's worth, I managed to compile everything and make it run, again by reconnecting the csproj of the NpmRegistry.Wrapper, setting it to .NET10, but keeping the dependencies on .NET9..........I have no clue why or how, but it does work with the rest as-is, as in using the JsonSerializerContexts provided. Did you make any progress yourself?

@MarcelVersteeg
Copy link
Owner

MarcelVersteeg commented Feb 8, 2026

@NPatch

For what it's worth, I managed to compile everything and make it run, again by reconnecting the csproj of the NpmRegistry.Wrapper, setting it to .NET10, but keeping the dependencies on .NET9..........I have no clue why or how, but it does work with the rest as-is, as in using the JsonSerializerContexts provided. Did you make any progress yourself?

That is good to hear. Unfortunately, I did not have any progress myself yet. As it is for a personal (non work-related project), time is currently very limited as I am also in the middle of a home renovation which takes more time to manage than anticipated.

However it is quite strange that just referencing the NpmRegistry.Wrapper project (instead of the NuGet package) works when setting the project to .NET10. When looking into it earlier I did the following (next to other things BTW);

  1. Fork and clone the NpmRegistry.Wrapper repository
  2. Set a new version number in the project
  3. Compile the project for .NET10
  4. Changed my NuGet.config to also load NuGet packages from the binary output folder of the NpmRegistry.Wrapper project
  5. Changed the package reference to the version I set in step 2.

This still resulted in the same MissingMethodException. It is very strange that directly referencing the csproj and setting it to .NET10 fixes the issue. It indeed seems that something is not completely correct in the NuGet package itself then for whatever reason.

@MarcelVersteeg
Copy link
Owner

@NPatch

I finally had some time to dig into this issue further. I added some additional logging to my code (did not update this repo) to see what could be wrong.

Without applying any of your patches, I tried to look into the differences in the runtime when running the task as a standalone application (which succeeds) and running it as an MSBuild task (which fails). To get a clear understanding in the differences, I created the following table:

Standalone app MSBuild task
Execution result succeeds fails with MissingMethodException
# of AssemblyLoadContext instances 1 2
NpmRegistry.Wrapper loaded in assembly load context Default assembly load context MSBuild plugin
System.Net.Http.Json loaded in assembly load context Default assembly load context Default

As can be seen in the table above, when the code is run as an MSBuild task, the NpmRegistry.Wrapper and System.Net.Http.Json assemblies are loaded into different assembly load contexts, which probably causes the fact that the methods from System.Net.Http.Json cannot be resolved.

The thing that I also see, is that in case it runs as an MSBuild task, all direct assembly references of my Tasks assembly are loaded into the MSBuild plugin load context and some of the indirect references are loaded into the Default load context. When I explicitly load the System.Net.Http.Json assembly into the MSBuild plugin load context before I access any method in NpmRegistry.Wrapper, the code also succeeds when running as an MSBuild task.

Now the question is, why is System.Net.Http.Json loaded into the Default load context, instead of the MSBuild plugin load context? When I explicitly load System.Net.Http.Json into the correct load context, I need to use AssemblyLoadContext.LoadFromAssemblyPath instead of AssemblyLoadContext.LoadFromAssemblyName as the latter will always load the assembly into the Default assembly load context.

I will also update my issue on MSBuild with this information. Maybe it will lead to some fix over there. So far I don't see any way to nicely solve it, except by explicitly loading the assembly into the correct load context.

@NPatch
Copy link
Author

NPatch commented Feb 19, 2026

I'm currently away from my machine, but both times it worked for me, I moved the package wrapper into a csproj in the project. If the way nuget and the embedded csproj work in similar ways where the nuget's http json ref goes to the default context whereas the embedded's ref gets loaded in the right context, it would make sense.

@NPatch
Copy link
Author

NPatch commented Feb 19, 2026

Looked into it a bit. Since MSBuild 16, Tasks get their own ALC (AssemblyLoadContext) and they are expected to keep all their dependencies there to avoid any conflicts. Another reason is so you can use different versions of the same lib in different Tasks without conflicts again. In your case, I see one possibly impactful issue. Your task is running out-of-proc due to .net10. I'm also not sure if the wrapper nuget keeps its own dependencies next to it, in the C:\Users\<you>\.nuget\packages\fiedler.npmregistry.wrapper\1.0.1\lib. AssemblyLoadContext.LoadFromAssemblyName seems to first look next to the Task lib itself and if not, #2 step is to try and load the assembly into the Default context. So there might be something there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants