Title image of Gather .Net versions and Nuget packages in C#

Gather .Net versions and Nuget packages in C#

2 March 2023

·
C#

No one likes doing developer admin like updating packages. We’re much happier focusing on new features and forgetting about legacy code. But unfortunately, it’s quite important. We need to make sure our code isn’t running on frameworks with security vulnerabilities.

So how do we keep on top of it? 🤔

Just tracking the framework versions in use is hard and keeping stuff updated is even harder. Doing it every time a new feature is added to a project is a good idea. But some repositories can go years without anyone looking at them. Staying updated is especially important now that .Net is being released annually and .Net has clear end-of-life dates. Doing small annual updates is easier than missing versions. Every version you miss increases your chances of having to make major changes to update.

Post objective: Create a simple app to gather all .Net versions and Nuget packages being used. Written in C# of course.

Repositories

Our app is going to need access to all of our repositories in the same location. I’ve written a previous post on how to clone all repositories from Azure DevOps which could help.

.Net versions

The .Net version of a project is stored in the .csproj file. This is an XML file so any XML parser can be used to extract the bits we need. But Microsoft provides an official Nuget package for interacting with these files: Microsoft.Build.

To do everything we want we need to install these three packages:

<PackageReference Include="Microsoft.Build" Version="17.4.0" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.5.5" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.4.0" ExcludeAssets="runtime" />

Our console app is written in .Net 6.0 which currently contains a bug with Microsoft.Build which often results in this error:

System.IO.FileNotFoundException: 'Could not load file or assembly 'Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. The system cannot find the file specified.'

The fix (described in this StackOverflow answer) is to run MSBuildLocator.RegisterDefaults() first then the rest of the Microsoft.Build stuff in a separate method. Doing this stops the JIT compiler from messing everything up under the hood. Here’s what the program.cs needs to look like:

internal class Program
{
	static void Main(string[] args)
	{
		MSBuildLocator.RegisterDefaults();

		GatherDependencies();
	}

	private static void GatherDependencies()
	{
		// Run Microsoft.Build stuff here
	}
}

Now we have the right structure to get Microsoft.Build working let’s fill it out.

Our code grabs every .csproj from the target folder, loads it with Microsoft.Build and then gets the target framework property:

var projects = Directory
    .EnumerateFiles(targetFolder, "*.csproj", SearchOption.AllDirectories);

foreach (var project in projects)
{
	var buildEngine = new ProjectCollection();
	var projectBuild = buildEngine.LoadProject(project);
	
	var targetFramework = projectBuild.GetPropertyValue("TargetFramework");
	
	// Old projects use TargetFrameworkVersion
	if (string.IsNullOrWhiteSpace(targetFramework))
	{
	    targetFramework = projectBuild.GetPropertyValue("TargetFrameworkVersion");
	}
	
	results.TryAdd(Path.GetFileName(project), targetFramework);
}

This will add all of the .Net versions into a ‘netVersions’ dictionary.

NuGet dependencies

Nuget references are also stored in the csproj so we can use Microsoft.Build again to get them.

This is how they’re stored in the csproj under a ItemGroup:

<ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.1" />
    <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
    <PackageReference Include="System.Drawing.Common" Version="7.0.0" />
</ItemGroup>

When Microsoft.Build loads a project all of the ItemGroups get combined into a single Items list. We can loop through all the items with the type “PackageReference” to get the package names and versions:

foreach(var package in projectBuild.Items.Where(x => x.ItemType == "PackageReference"))
{
    packages.Add(new PackageReference
    {
        Project = Path.GetFileName(project),
        PackageName = package.EvaluatedInclude,
        PackageVersion = package.GetMetadataValue("Version")
    });
}

All the package names and versions then get added into a ‘packages’ list.

Finished

We now have all of the .Net versions inside a ‘netVersions’ dictionary and all of the package dependencies in a ‘packages’ list. Running the console app gives this output:

The complete tidied-up code can be found on Github:

https://github.com/Liam-Hunt/NetVersionTracker