How to Use NuGet Packages in Source Generators
Transcript
In this video I'm going to take you through how to add NuGet packages to a source generator that you might be working on. Now there's a few steps involved here and a couple of different approaches, but I'm going to break them down very simply for you. Please pay attention to both approaches because one does make the other a little bit more automated, but there is a caveat that you need to be aware of as well.
So if I take a look at the project that we've got here today, much like our previous projects, this source generator that I have doesn't really do much. Apart from we've got a serialized object using Newtonsoft.Json and we're just going to serialize this to a file that will be generated as part of our output. So if we take a look at the generator project ourselves, we have the package reference which includes Newtonsoft.Json at version 13, and we're only using it for the purposes of generation so we're not going to be including this as a dependency of the generator itself in this case.
Now the important point to note here is that we're using the GeneratePathProperty equals true, and what this does is it generates a new variable called Pkg and then the package name with any periods replaced with an underscore. So we can see this at the bottom of the screen: PkgNewtonsoft_Json.
So when we have a dependency that we want to use, we first need to edit the GetTargetPathDependsOn property. We set it to itself and then we append a brand new target, which in this case we're just going to call GetDependencyTargetPaths. You can call it whatever you want so long as it doesn't conflict with anything. And inside this target, what we need to do is specify the TargetPathWithTargetPlatformMoniker. Yes that is a mouthful to say. And basically what this does is it tells the compiler where to go and look for certain files for which target platforms. To do this we're going to use the variable that we had output earlier, and then because this property is just the root of essentially the NuGet package on disk, we then need to search inside of the NuGet package itself. In which case we're going to be using lib/netstandard2.0/Newtonsoft.Json.dll. The last thing that we need to do is include the RuntimeDependency equals false.
Now if you're planning to use your source generator in a NuGet package rather than a library reference like I've got here, there's a couple of extra steps that you need to do. First, we need to include the DLL into the NuGet package itself but put it into the analyzers/dotnet/cs path. We then need to go and do the same for all of the dependencies as well. So in this case I only have a single dependency which is Newtonsoft.Json. So I copy the same DLL path as above up until the point where I specify the DLL name and then I use a star to say grab everything that's in that folder that is a DLL, package it, and put it alongside the analyzer. Now because we have already included the assembly output here, we do need to make one additional change and that's right at the top in our property group where we say don't include the build output. We do this by saying IncludeBuildOutput equals false.
Now if Newtonsoft.Json had dependencies of itself then we would go about it like this. We would copy line 34 and change the variables, such as the NuGet package variable to whatever the dependency is, change the target framework moniker if it's required, change your DLL name, and then make the same changes on a copied version on line 38.
So now if I build my main project using the integrated terminal, we can see that everything works as we're expecting. Because I'm cross-compiling I get the net5.0 and net6.0 folders, but we can see my JSON is output as expected.
So now let's go over to this slightly more automated solution. To do this we're going to go back to the generator project. We still have the IncludeBuildOutput set to false if you're having a NuGet package made out of this generator. Nothing changes on the package reference, so we still have the GeneratePathProperty equals true. Remember, this is the most important part of this because this generates the Pkg property. We still have our edited target groups but then we've got a few small changes here. So the first one is we've included this AfterTargets and we set this to a target called ResolvePackageDependenciesForBuild. This is one of the built-in targets and we're just ensuring our target runs after that target. And the reason is because we need this ResolvedCompileFileDefinitions property. Now if you're not familiar with the MSBuild kind of files, this basically says use all the elements inside of there by using kind of like an array syntax. And what this means is if we had multiple dependencies, such as Newtonsoft.Json and then the dependencies of that, this way of doing things would automatically include all of those.
Now that might seem like a great idea to you, but what you need to bear in mind is when it comes around to packaging your source generator, then this will actually include more than the one or two DLLs that you would think you would include. Now with proper testing, this may or may not be a problem for your scenario. But from a safety point, even though it's slightly more work, I do prefer the manual approach. And that's the approach that I've used in personal projects.
Just to round off everything, I'll show you that it does work as expected. We'll go to the main project, run dotnet build again, and you'll see in one of our folders we have our generated output.
If you enjoyed this video, consider subscribing to the YouTube channel for more content like this.