roslyn: Source generator dependency unable to be resolved

Version Used: MSBuild: 16.9.0.11203 .NET: 5.0.201 CSC: 3.900.21.12328 (comes with .NET 5.0.201 and used by dotnet build), 3.900.21.16010 (comes with Visual Studio and used at least by msbuild but probably VS too) - All the commits between these two versions Visual Studio: 16.9.2

Steps to Reproduce / Actual Behaviour: Sample project to recreate issue: https://github.com/Turnerj/SourceGeneratorDependencyTest

Using .NET CLI (like we use on a CI)

  1. Clean solution
  2. Build solution with command dotnet build
  3. Solution builds 🎉

Using MSBuild

  1. Clean solution
  2. Build solution with command msbuild
  3. Solution fails (see generator warning) 😕

Using Visual Studio

  1. Clean solution
  2. Build solution via Visual Studio
  3. Solution fails (see generator warning) 😞

Generator Warning

CSC : warning CS8784: Generator ‘CustomSourceGenerator’ failed to initialize. It will not contribute to the output and compilation errors may occur as a result. Exception was of type ‘FileNotFoundException’ with message ‘Could not load file or assembly ‘System.Text.Encodings.Web, Version=5.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51’ or one of its dependencies. The system cannot find the file specified.’

Expected Behavior:

Each method builds the project.

Notes:

The sample project is a minimum example of what we use in a full project: https://github.com/RehanSaeed/Schema.NET/ I’m following the guidance listed in this discussion about how to mark the package reference: https://github.com/dotnet/roslyn/discussions/47517#discussioncomment-64145 This is also seen on the Roslyn SDK C# samples: https://github.com/dotnet/roslyn-sdk/blob/0313c80ed950ac4f4eef11bb2e1c6d1009b328c4/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.csproj#L13-L30 I’ve added the transient dependency (in my case System.Text.Encodings.Web) as per this comment in the same discussion: https://github.com/dotnet/roslyn/discussions/47517#discussioncomment-277339

If I use the version 5.0.0 of System.Text.Encodings.Web, it does build with all methods. I’ve tried to debug binlogs of this but I haven’t found anything explaining why it resolves correctly via dotnet build and not via Visual Studio (or msbuild).

I don’t see why I can’t specifically use version 5.0.1 of System.Text.Encodings.Web, hopefully you can work out what is going on!

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 37 (7 by maintainers)

Most upvoted comments

I’ve been looking into ways that a user can avoid manually specifying transient dependencies for a source generator and have something working with the following:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  
  <ItemGroup Label="Package References">
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
    <PackageReference Include="System.Text.Json" Version="5.0.1" PrivateAssets="all" />
  </ItemGroup>

  <PropertyGroup>
    <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
  </PropertyGroup>

  <Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
    <ItemGroup>
      <TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
    </ItemGroup>
  </Target>
</Project>

This effectively adds:

~\.nuget\packages\microsoft.bcl.asyncinterfaces\5.0.0\lib\netstandard2.0\Microsoft.Bcl.AsyncInterfaces.dll
~\.nuget\packages\microsoft.codeanalysis.common\3.9.0\lib\netstandard2.0\Microsoft.CodeAnalysis.dll
~\.nuget\packages\microsoft.codeanalysis.csharp\3.9.0\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.dll
~\.nuget\packages\system.buffers\4.5.1\ref\netstandard2.0\System.Buffers.dll
~\.nuget\packages\system.collections.immutable\5.0.0\lib\netstandard2.0\System.Collections.Immutable.dll
~\.nuget\packages\system.memory\4.5.4\lib\netstandard2.0\System.Memory.dll
~\.nuget\packages\system.numerics.vectors\4.5.0\ref\netstandard2.0\System.Numerics.Vectors.dll
~\.nuget\packages\system.reflection.metadata\5.0.0\lib\netstandard2.0\System.Reflection.Metadata.dll
~\.nuget\packages\system.runtime.compilerservices.unsafe\5.0.0\ref\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll
~\.nuget\packages\system.text.encoding.codepages\4.5.1\lib\netstandard2.0\System.Text.Encoding.CodePages.dll
~\.nuget\packages\system.text.encodings.web\5.0.0\lib\netstandard2.0\System.Text.Encodings.Web.dll
~\.nuget\packages\system.text.json\5.0.1\lib\netstandard2.0\System.Text.Json.dll
~\.nuget\packages\system.threading.tasks.extensions\4.5.4\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll

This isn’t perfect as it adds a few other libraries against TargetPathWithTargetPlatformMoniker that shouldn’t be there but with some more time processing the available data in the custom target, it would be possible to have this configured without any issues.

My question though goes back to: Why isn’t something like this done automatically for projects with source generators anyway?

Honestly, I don’t know really if this is much a Roslyn issue now or a default targets issue with MSBuild or .NET itself. I want to push for having this automatically handled by some part of the build process so users don’t need to bother with manually configuring it.

Any advice on where this would be best to further discuss would be appreciated!

@aktxyz Thank you for that information. My situation must be different since that solution doesn’t fully work for me. In the case of locally referencing an analyzer that can work but when generating a Nuget package it seems required to manually specify transitient dependencies.

Additionally, my use of the AdhocWorkspace type ends up meaning that compilation of the source generator fails without also adding:

<ItemGroup>
<PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="3.9.0-2.final">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

This is such a confusing feature of C#. It seems very difficult to use source generators due to all these issues. Hopefully this is all resolved one day by Microsoft.

Source Generators also have different behavior with assembly loading/redirection when generating nugets vs local project references. It’s actually an absolute headache really.

There isn’t a restriction here on having dependencies just a requirement that source generators deploy their dependencies with them.

I guess my main issue here is that where one would normally just install the specific dependency they need, for a source generator I need to specify the entire tree of dependencies in my csproj file manually. While I’m sure a mountain of work has gone into making source generators work, this (and debugging source generators) make it feel somewhat incomplete.

While @jmarolf went out of their way to list all of the dependencies in my case (thank you!), basically I’m wondering why some combination of NuGet restore/compilation process isn’t doing this automatically. NuGet restore already has to work out all the dependencies anyway however this current process of manually adding everything is quite intensive, especially for when package updates happen as I would need to re-check to see if any new dependencies are added.

So would there be any plans on changing this behaviour and having the compiler (or some other part of the build process) automatically handle this?

I literally can’t get this to work no matter what I do. I’ll have to switch to another Json package and lick my wounds. This is not worth my time and I wouldn’t expect anyone to be an expert in the inner workings of msbuild/csproj.

It would be great if someone could update the section on nuget dependencies in the cookbook to include this: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md

Hmm, this has got me thinking though. @jmarolf it looks like ResolvePackageAssets is explicitly spitting out the analyzers, which the compiler consumes. Presumably at this point (or earlier on when we write the cache file) we have access to the assets.json which describe the analyzer dependencies?

Can we just hook that task to return the dependencies as part of the analyzers directly (or even better, as an AnalyzerDependencies item group that we can merge later on to make debugging easier?)

What @Turnerj recommended did not work for me:

Exception was of type 'FileLoadException' with message 'Could not load file or assembly 'System.Text.Json, Version=5.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. Could not find or load a specific file. (0x80131621)'

This is a source generator used against a .net 5 project.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <LangVersion>preview</LangVersion>
        <BuildOutputTargetFolder>analyzers</BuildOutputTargetFolder>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" />
        <PackageReference Include="Humanizer.Core" Version="2.10.1" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="5.0.0" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Buffers" Version="4.5.1" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Memory" Version="4.5.4" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Numerics.Vectors" Version="4.5.0" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Text.Encodings.Web" Version="5.0.1" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Text.Json" Version="5.0.2" GeneratePathProperty="true" PrivateAssets="all" />
    </ItemGroup>
    
    <PropertyGroup>
        <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
    </PropertyGroup>
    
    <Target Name="GetDependencyTargetPaths">
        <ItemGroup>
            <TargetPathWithTargetPlatformMoniker Include="$(PKGHumanizer_Core)\lib\netstandard2.0\Humanizer.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Text_Json)\lib\netstandard2.0\System.Text.Json.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Text_Encodings_Web)\lib\netstandard2.0\System.Text.Encodings.Web.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Buffers)\lib\netstandard2.0\System.Buffers.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Memory)\lib\netstandard2.0\System.Memory.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Numerics_Vectors)\lib\netstandard2.0\System.Numerics.Vectors.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Runtime_CompilerServices_Unsafe)\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Threading_Tasks_Extensions)\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGMicrosoft_Bcl_AsyncInterfaces)\lib\netstandard2.0\Microsoft.Bcl.AsyncInterfaces.dll" IncludeRuntimeDependency="false" />
        </ItemGroup>
    </Target>
</Project>

This bug has been open for nearly two years, are there any official updates on this issue? All of the solutions above seem to be unreliable workarounds–I haven’t personally gotten them to work.

But it made me take a step further than your solution and wonder why not just do this:

<Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
    <ItemGroup>
      <None Include="@(ResolvedCompileFileDefinitions)" Pack="true" PackagePath="analyzers/dotnet/cs" />
    </ItemGroup>
</Target>

And it just works 😃

Glad you got something working for yourself @HavenDV ! In my cases with source generators, they have been local projects and are only to aid the build of a different local project rather than packing the dependencies.

Still hoping for some movement on dotnet/sdk#17775 to automate all of this though AFAIK there are various blockers that are preventing progress. Fingers crossed for a .NET 7 fix!

@Turnerj

To work around it, I’ve found forcing a full solution rebuild immediately after it fails to build to works around this problem.

The ResolvePackageDependenciesForBuild target is involved in restore so won’t occur in future builds which is probably the behaviour you’re seeing. I (think) you can substitute it for ResolvePackageAssets and it should fix it.

This isn’t perfect as it adds a few other libraries against TargetPathWithTargetPlatformMoniker that shouldn’t be there but with some more time processing the available data in the custom target, it would be possible to have this configured without any issues.

As you said above however, the trouble with this approach is it doesn’t discriminate and basically adds _everything _ as an analyzer dependency. For small projects or things under your control that might be ok, but will slow the compilation as the list gets bigger, and worse potentially lead to really hard to debug version mismatches when you start adding more dependencies.

More importantly than that for me though, is some sort of automated behaviour by the compiler for adding transient dependencies to TargetPathWithTargetPlatformMoniker something the compiler team would even consider

Its definitely on our radar, and something that has come up more since the introduction of source generators. The problem existed with analyzers too, but authors were just less likely to use dependencies. We’ve been discussing a range of steps that would improve all these scenarios. Most of that work is going to be outside of Roslyn (though we’ll obviously help) so you’re probably best opening an issue in http://github.com/dotnet/sdk to get the ball rolling.

That level of MSBuild magic is a bit beyond me. @chsienki can likely better say.

I’ve been having the issue myself @kamronbatman . I’m having trouble tracking what the TargetPathWithTargetPlatformMoniker actually does. In some cases it seems to work, in others, it doesn’t.

I’m creating a custom NuGet repo on disk, and I have a project called “My.Analyzers.Extensions”

I have another project, which is an analyzer, called “My.Microservice.Builder.Analyzers” which references System.Text.Json, Microsoft.Bcl.AsyncInterfaces, and My.Analyzers.Extensions.

When I add
<TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />, then my My.Analyzer.Extensions dll has the same FileNotFound error.

However I have a workaround that seems to work, that uses the <None Include> tag, which effectively packages a dll in the package. I’m concerned that it means that if the parent library that inherits this package has a different version, this analyzer will NOT use the different version, but will continue to use the packaged version. Is that Works as Designed?

Here’s my workaround that only packages what you need. You just need to add 'Pack=“true” ’ to the PackageReference.

It uses a combination of @Turnerj and @kzu’s blog post: https://til.cazzulino.com/msbuild/how-to-include-package-reference-files-in-your-nuget-package

    <Target 
      Name="AddGenerationTimeReferences" 
      BeforeTargets="GetTargetPathWithTargetPlatformMoniker"
      AfterTargets="ResolvePackageDependenciesForBuild"
      Outputs="%(ResolvedCompileFileDefinitions.NuGetPackageId)">
        <ItemGroup>
            <NuGetPackageId Include="@(ResolvedCompileFileDefinitions -> '%(NuGetPackageId)')" />
        </ItemGroup>
        <PropertyGroup>
            <NuGetPackageId>@(NuGetPackageId -&gt; Distinct())</NuGetPackageId>
        </PropertyGroup>
        <ItemGroup>
            <PackageReferenceDependency Include="@(PackageReference -> WithMetadataValue('Identity', $(NuGetPackageId)))"/>
        </ItemGroup>
        <ItemGroup>
          <PackableReferenceDependency Include="@(PackageReferenceDependency -> WithMetadataValue('Pack', 'true'))"/>
        </ItemGroup>
        <PropertyGroup>
          <PackableReferenceDependency>@(PackableReferenceDependency -&gt; Distinct())</PackableReferenceDependency>
        </PropertyGroup>
        <ItemGroup>
            <ResolvedPackableCompileFileDefinitions
              Include="@(ResolvedCompileFileDefinitions -&gt; WithMetadataValue('NuGetPackageId', '$(PackableReferenceDependency)'))" />
        </ItemGroup>
        <ItemGroup>
            <!-- <TargetPathWithTargetPlatformMoniker Include="@(ResolvedPackableCompileFileDefinitions)" IncludeRuntimeDependency="false" /> -->
            <None Include="@(ResolvedPackableCompileFileDefinitions)" Pack="true" PackagePath="analyzers/dotnet/cs" />
        </ItemGroup>
    </Target>

I would appreciate it if you would complete your article with this workaround for NuGet. I think it would save time for some people like me. I manually maintained a large number of transient dependencies for about a year.

I am on the latest vs 16.10.0 … and the relevant sections of my csproj look like this … hope this helps

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>preview</LangVersion>
    <NoWarn>1701;1702;1591;1998;CS0162;CS0168;CS0618;EF1001</NoWarn>
    <RootNamespace>hmm</RootNamespace>
    <LangVersion>8.0</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Humanizer.Core" Version="2.10.1" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
    <PackageReference Include="System.Text.Json" Version="5.0.0" />
  </ItemGroup>

  <PropertyGroup>
    <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
  </PropertyGroup>

  <Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
    <ItemGroup>
      <TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
    </ItemGroup>
  </Target>

woohoo ! thanks !

at first I switched to to 5.0.0 and was still getting the same exception when calling JsonSerializer.Serialize

<PackageReference Include="System.Text.Json" Version="5.0.0" />

BUT … then I decided to try adding this incantation to the csproj and BOOM it worked.

  <PropertyGroup>
    <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
  </PropertyGroup>

  <Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
    <ItemGroup>
      <TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
    </ItemGroup>
  </Target>

Now, should I spend my day learning about the above stuff (TargetPathWithTargetPlatformMoniker/etc) …

or actually making progress on the project … hmm … project it is !

I didn’t read anywhere about passing dependencies via a /analyzer so I am not even sure how to do it. Can you show me an example?

All DLLs in the same folder as the analyzer will essentially be passed via /analyzer automatically. It’s essentially implicit. Generally if the package has the right dependencies it will just work.

This is why I was asking for the binlog though. It will make it apparent what is happening here.