There are a few issues at play here which made debugging this issue quite complex. I'll break my answer into parts which hopefully provide a bit of signposting for future readers about how to go about debugging something like this.
Part 1 - The warning
You mentioned you got this warning:
CSC : warning CS8032: An instance of analyzer SuperFluid.Internal.SourceGenerators.FluidApiSourceGenerator cannot be created from /home/james/.nuget/packages/superfluid/0.0.1/analyzers/dotnet/cs/SuperFluid.dll : Exception has been thrown by the target of an invocation.. [/home/james/repos/SuperFluid/src/DemoProject/DemoProject.csproj]
It does seem a bit unhelpful at first, but there is some key information:
An instance of ... FluidApiSourceGenerator cannot be created
This means that the compiler is trying to create an instance of your generator and failing at that point. It's not even getting to the point of calling Initialize
or running the source generator pipeline.
So you should be looking in your source generator's constructor, and any properties/fields that are initialised inline.
Part 2 - Getting more information from the TargetInvocationException
The second part of your question deals with the "Exception has been thrown by the target of an invocation" part of the warning.
This exception is being thrown when the compiler process tries to instantiate your class using reflection. Ideally we need to move the error-prone code to somewhere where we can log the exception somehow.
One option is to temporarily move the constructor logic into the Initialize
method so that we have control over how the exception is handled, and can "log" it using ReportDiagnostic
, so the real exception (not the TargetInvocationException) comes out as a compiler error during the build process.
(for future readers, the below code is based on the full generator source, from the repository provided in this extended discussion)
Something like this:
[Generator]
internal class FluidApiSourceGenerator : IIncrementalGenerator {
// Remove temporarily
// private readonly FluidGeneratorService _generatorService;
// 1. Add a descriptor for when we need to output fatal errors
private static readonly DiagnosticDescriptor FatalErrorOccurred = new(
id: "E1",
title: "Error occurred",
messageFormat: "Error: '{0}'",
category: "CodeGeneration",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Error occurred during code generation.");
public FluidApiSourceGenerator() { }
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<AdditionalText> extraTexts = context.AdditionalTextsProvider.Where(f => f.Path.EndsWith(".fluid.yml"));
IncrementalValuesProvider<(string Name, string Content)> namesAndContents = extraTexts.Select((text, cancellationToken) => (Name: Path.GetFileNameWithoutExtension(text.Path), Content: text.GetText(cancellationToken)!.ToString()));
context.RegisterSourceOutput(namesAndContents, (context, nameAndContent) => {
try {
// 3. Call the constructor logic to get an instance of FluidGeneratorService,
// catching any exceptions
var _generatorService = ConstructorLogic();
Dictionary<string, string> generatedSource = _generatorService.Generate(nameAndContent.Content);
foreach (KeyValuePair<string, string> kvp in generatedSource) {
context.AddSource(kvp.Key, kvp.Value);
}
} catch (Exception e) {
// 4. Report exceptions as compiler errors.
var diagnostic = Diagnostic.Create(FatalErrorOccurred, Location.None, e.ToString());
context.ReportDiagnostic(diagnostic);
}
});
}
// 2. Move the logic that used to be in the constructor, to here
private FluidGeneratorService ConstructorLogic() {
IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(NullNamingConvention.Instance).Build();
return new FluidGeneratorService(deserializer, new());
}
}
Doing this, we see that the real exception is:
'System.IO.FileNotFoundException: Could not load file or assembly 'YamlDotNet, Version=13.0.0.0, Culture=neutral, PublicKeyToken=ec19458f3c15af5e'. The system cannot find the file specified.
Edit - Extracting the constructor logic into a separate method (rather than just doing it inside the try
) is actually important. This is because of the nature of this specific issue (dependency not being found).
Dependencies used within a method are all linked the first time the method is called, but before any code inside is executed. This means that if we do the constructor logic "inline" within the try
block, the exception will be thrown before we get into the try block, so we won't catch it.
See this answer.
Part 3 - Resolving the FileNotFoundException
To use 3rd party Nuget packages, Roslyn source generators must have those packages bundled as private dependencies.
I downloaded your package from Nuget, and YamlDotNet.dll
isn't in there, which explains the FileNotFoundException
:

Now, when I cloned your repository and built your SuperFluid
project, the generated package did include all the dependencies:

Based on that, I suspect there's something in your CD pipeline that's not playing nice. I don't have access to that configuration so I can't give you a definitive answer on that, but I can tell you what seems to be working for me:
- Make sure you include
GeneratePackageOnBuild
in your build file:
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<!-- other stuff -->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
</PropertyGroup>
- Make sure the
PackageReference
for your 3rd party dependency includes PrivateAssets="all"
and GeneratePathProperty="true"
(I don't think this is directly relevant to the issue at hand, but it is best practice to ensure that you're not introducing your generator dependencies as dependencies on the consuming project)
<PackageReference Include="YamlDotNet" Version="13.1.1" PrivateAssets="all" GeneratePathProperty="true" />
- Make sure you're copying the dependencies to the analyzers/dotnet/cs folder (the "gross hack" from your question):
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
<None Include="@(ResolvedCompileFileDefinitions)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Target>
Once all that is done - which in your case, it is, run dotnet build
(or build via your IDE) to build the generator (not the consuming project / demo project). This will create a .nupkg
file:
bin/Release/SuperFluid.0.0.1-alpha.nupkg
Then do whatever you need to do with this file - publish to NuGet, install locally, whatever.
As for configuring your CD pipeline with this in mind, that's beyond the scope of discussion here, so I'll leave that to you.
Part 3.1 - The FileNotFoundException in the Demo Project
As a bonus - there was actually a separate issue causing the same result in the Demo Project, which had to do with how the ProjectReference
was set up:
<ProjectReference Include="..\SuperFluid\SuperFluid.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
Remove SetTargetFramework="TargetFramework=netstandard2.0"
and it will work.
I'm not 100% sure why dependencies aren't copied correctly when this is present, but the SetTargetFramework
property doesn't really apply here:
SetTargetFramework
is used when referencing multi-targeted dependencies. So, if SuperFluid targeted both netstandard2.0 and net7.0, you would use SetTargetFramework
to pick which one you wanted.
- It being present seems to cause the issue you're seeing when
SuperFluid
only targets one framework (which, being a source generator, is all it's allowed to target).
- Interestingly, adding a second target framework to
SuperFluid
(even though this is not supported), actually makes the problem go away as well - so dependencies apparently get copied correctly when SetTargetFramework
is used correctly, but don't when it's not. If anyone knows why this happens, I'd love to know.
- Like I said before though, source generators should target netstandard2.0, so the solution is for
SuperFluid
to target a single framework, and for SetTargetFramework
to be removed.