Source Generators dependencies not loaded in Visual Studio
Asked Answered
K

4

10

I am working on source generator and I have problems with dependencies:

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 'Flurl.Http, Version=3.0.1.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.'

There is a lot of information on how to pack dependencies into nuget, but I reference analyzer project directly like this:

<ProjectReference Include="SG.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

In analyzer project I added <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> and all dependencies are available in output directory, but VS is not using that directory - it uses AppData\Local\Temp\VBCSCompiler\AnalyzerAssemblyLoader\[...] instead and it copies only one DLL there.

What can be done to make that work?

Kandace answered 13/4, 2021 at 8:33 Comment(0)
K
8

I found the way to make it work more or less reliably with some hacks.

Before that I also tried ILMerge, but it didn't work (missing method exceptions).

Solution:

First of all I embeded dependencies in source generator assembly like this:

<ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
    <EmbeddedResource Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" Visible="false" />
</ItemGroup>

Then I created AssemblyResolve handler for AppDomain (static constructor in generator class) like so:

AppDomain.CurrentDomain.AssemblyResolve += (_, args) =>
{
    AssemblyName name = new(args.Name);
    Assembly loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == name.FullName);
    if (loadedAssembly != null)
    {
        return loadedAssembly;
    }

    string resourceName = $"Namespace.{name.Name}.dll";

    using Stream resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
    if (resourceStream == null)
    {
        return null;
    }
    
    using MemoryStream memoryStream = new MemoryStream();
    resourceStream.CopyTo(memoryStream);

    return Assembly.Load(memoryStream.ToArray());
};
Kandace answered 13/4, 2021 at 11:29 Comment(0)
R
9

The way that this should work is outlined in the source-generators cook-book, with their example being:

<Project>
  <PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
    <IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
  </PropertyGroup>

  <ItemGroup>
    <!-- Take a private dependency on Newtonsoft.Json (PrivateAssets=all) Consumers of this generator will not reference it.
         Set GeneratePathProperty=true so we can reference the binaries via the PKGNewtonsoft_Json property -->
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" PrivateAssets="all" GeneratePathProperty="true" />

    <!-- Package the generator in the analyzer directory of the nuget package -->
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

    <!-- Package the Newtonsoft.Json dependency alongside the generator assembly -->
    <None Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>
</Project>

However, it is my experience that this is still not reliable or robust; this is acknowledged, and I get the impression that improving this experience is part of future planning, but: right now, honestly it is better off simply not using dependencies outside of the core framework and the Roslyn libraries that are already loaded. If your dependencies are in the same repo as the analyzer/generator, you might be able to simply suck in the code during build, for example from here:

<ItemGroup>
    <!-- compile what we need from protobuf-net directly; package refs cause pure pain in anaylizers-->
    <Compile Include="../protobuf-net.Core/**/*.cs" Link="protobuf-net.Core"/>
    <Compile Remove="../protobuf-net.Core/obj/**/*.cs" />
    <Compile Include="../protobuf-net.Reflection/**/*.cs"  Link="protobuf-net.Reflection"/>
    <Compile Remove="../protobuf-net.Reflection/obj/**/*.cs" />
</ItemGroup>
Rundown answered 13/4, 2021 at 8:46 Comment(1)
I tried their way, but it seems to work only when analyzer is included as nuget not as a project reference. Unfortunately I want to include external dependencies (Newtonsoft.Json for example) so source code linking wan't work for me.Kandace
K
8

I found the way to make it work more or less reliably with some hacks.

Before that I also tried ILMerge, but it didn't work (missing method exceptions).

Solution:

First of all I embeded dependencies in source generator assembly like this:

<ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
    <EmbeddedResource Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" Visible="false" />
</ItemGroup>

Then I created AssemblyResolve handler for AppDomain (static constructor in generator class) like so:

AppDomain.CurrentDomain.AssemblyResolve += (_, args) =>
{
    AssemblyName name = new(args.Name);
    Assembly loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == name.FullName);
    if (loadedAssembly != null)
    {
        return loadedAssembly;
    }

    string resourceName = $"Namespace.{name.Name}.dll";

    using Stream resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
    if (resourceStream == null)
    {
        return null;
    }
    
    using MemoryStream memoryStream = new MemoryStream();
    resourceStream.CopyTo(memoryStream);

    return Assembly.Load(memoryStream.ToArray());
};
Kandace answered 13/4, 2021 at 11:29 Comment(0)
B
4

ProjectReference is not the same as PackageReference!

The GeneratePathProperty will not take any action for ProjectReference because it is not a package.

So, you just have to add the TargetPathWithTargetPlatformMoniker pointing to your local DLL into Source Generator csproj.

Example with default output locations:

Folder structure

- Solution Folder
| - MySolution.sln
| + MyProject
| | - MyProject.csproj
| | + bin
| | | + Debug
| | | | + netstandard2.0
| | | | | - MyProject.dll
| | | + Release
| | | | + netstandard2.0
| | | | | - MyProject.dll
| + MySourceGenerator
| | - MySourceGenerator.csproj

MySourceGenerator.csproj

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

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>

        <IsRoslynComponent>true</IsRoslynComponent>
        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
    </ItemGroup>

    <ItemGroup>
        <ProjectReference Include="..\MyProject\MyProject.csproj" PrivateAssets="all"/>
    </ItemGroup>

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

    <Target Name="GetDependencyTargetPaths">
        <ItemGroup>
            <TargetPathWithTargetPlatformMoniker Include="..\MyProject\bin\$(Configuration)\netstandard2.0\MyProject.dll" IncludeRuntimeDependency="false"/>
        </ItemGroup>
    </Target>
    
</Project>
Brescia answered 9/2, 2023 at 16:10 Comment(0)
S
2

There is a definitive answer to this now. The Source Generator Samples contain an example .csproj which has the required setup.

I used this to make a source generator with Sprache, works great.

Pasting the example .csproj here for reference:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
</PropertyGroup>

<ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftNetCompilersToolsetVersion)" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
    <!-- Generator dependencies -->
    <PackageReference Include="CsvTextFieldParser" Version="1.2.2-preview" GeneratePathProperty="true" PrivateAssets="all" />
    <PackageReference Include="Handlebars.Net" Version="1.10.1" GeneratePathProperty="true" PrivateAssets="all" />
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>

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

<Target Name="GetDependencyTargetPaths">
    <ItemGroup>
      <TargetPathWithTargetPlatformMoniker Include="$(PKGCsvTextFieldParser)\lib\netstandard2.0\CsvTextFieldParser.dll" IncludeRuntimeDependency="false" />
      <TargetPathWithTargetPlatformMoniker Include="$(PKGHandlebars_Net)\lib\netstandard2.0\Handlebars.dll" IncludeRuntimeDependency="false" />
      <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
    </ItemGroup>
</Target>
</Project>
Scaffold answered 6/12, 2021 at 9:24 Comment(1)
I still haven't had luck with that. EmbeddedSource workaround is the only solution that works for me.Kandace

© 2022 - 2024 — McMap. All rights reserved.