When you are working with a nontrivial .NET solution you will eventually have a
lot of references between projects and NuGet-packages. Here is a little script
that finds references that are no longer needed in .NET Core projects.
The problem
Having references to projects or NuGet packages that is no longer needed will
make you project slower to build, and it might also be a bit slower to run. So,
removing redundant references is a good thing. But how do you find these
references?
The solution
My solution is to try to remove one reference at the time and then rebuild the
project. If it still works, it could probably be removed. Doing this manually is
very tedious, but with some PowerShell code it will not require much work.
The script below is doing just this, removing one reference as checks if the
project still compiles. When it has checked every reference, you will get a
summary.
Since it is modifying the project files (it will restore the file is nothing
unexpected happen) you should have a backup. Also, be aware that this will take
a while to run. Do not be surprised it takes more than one hour on a large
project.
Do not be surprised if you can not remove every reference that is suggested. Due
dependencies you may be able to remove one suggested reference but not two. The
script gives you suggestions about references that may be able to be removed. In
the end it is up to you to decide what to do with this.
Before you run the script, you need to change the path to directory that
contains all projects that should be checked.
function Get-PackageReferences {
param ( $FileName , $IncludeReferences , $IncludeChildReferences )
$xml = [ xml ] ( Get-Content $FileName )
$references = @()
if ( $IncludeReferences ) {
$packageReferences = $xml | Select-Xml -XPath "Project/ItemGroup/PackageReference"
foreach ( $node in $packageReferences )
{
if ( $node . Node . Include )
{
if ( $node . Node . Version )
{
$references += [ PSCustomObject ]@{
File = ( Split - Path $FileName - Leaf ) ;
Name = $node . Node . Include ;
Version = $node . Node . Version ;
}
}
}
}
}
if ( $IncludeChildReferences )
{
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
foreach ( $node in $projectReferences )
{
if ( $node . Node . Include )
{
$childPath = Join-Path -Path ( Split-Path $FileName -Parent ) -ChildPath $node . Node . Include
$childPackageReferences = Get-PackageReferences $childPath $true $true
$references += $childPackageReferences
}
}
}
return $references
}
function Get-ProjectReferences {
param ( $FileName , $IncludeReferences , $IncludeChildReferences )
$xml = [ xml ] ( Get-Content $FileName )
$references = @()
if ( $IncludeReferences ) {
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
foreach ( $node in $projectReferences )
{
if ( $node . Node . Include )
{
$references += [ PSCustomObject ]@{
File = ( Split - Path $FileName - Leaf ) ;
Name = $node . Node . Include ;
}
}
}
}
if ( $IncludeChildReferences )
{
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
foreach ( $node in $projectReferences )
{
if ( $node . Node . Include )
{
$childPath = Join-Path -Path ( Split-Path $FileName -Parent ) -ChildPath $node . Node . Include
$childProjectReferences = Get-ProjectReferences $childPath $true $true
$references += $childProjectReferences
}
}
}
return $references
}
$files = Get-ChildItem -Path C:\MySolutionDirectory -Filter *. csproj -Recurse
Write-Output "Number of projects: $( $files . Length ) "
$stopWatch = [ System.Diagnostics.Stopwatch ]:: startNew ()
$obseletes = @()
foreach ( $file in $files ) {
Write-Output ""
Write-Output "Testing project: $( $file . Name ) "
$rawFileContent = [ System.IO.File ]:: ReadAllBytes ( $file . FullName )
$childPackageReferences = Get-PackageReferences $file . FullName $false $true
$childProjectReferences = Get-ProjectReferences $file . FullName $false $true
$xml = [ xml ] ( Get-Content $file . FullName )
$packageReferences = $xml | Select-Xml -XPath "Project/ItemGroup/PackageReference"
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
$nodes = @( $packageReferences ) + @( $projectReferences )
foreach ( $node in $nodes )
{
$previousNode = $node . Node . PreviousSibling
$parentNode = $node . Node . ParentNode
$parentNode . RemoveChild ( $node . Node ) > $null
if ( $node . Node . Include )
{
$xml . Save ( $file . FullName )
if ( $node . Node . Version )
{
$existingChildInclude = $childPackageReferences | Where-Object { $_ . Name -eq $node . Node . Include -and $_ . Version -eq $node . Node . Version } | Select-Object -First 1
if ( $existingChildInclude )
{
Write-Output " $( $file . Name ) references package $( $node . Node . Include ) ( $( $node . Node . Version ) ) that is also referenced in child project $( $existingChildInclude . File ) ."
continue
}
else
{
Write-Host -NoNewline "Building $( $file . Name ) without package $( $node . Node . Include ) ( $( $node . Node . Version ) )... "
}
}
else
{
$existingChildInclude = $childProjectReferences | Where-Object { $_ . Name -eq $node . Node . Include } | Select-Object -First 1
if ( $existingChildInclude )
{
Write-Output " $( $file . Name ) references project $( $node . Node . Include ) that is also referenced in child project $( $existingChildInclude . File ) ."
continue
}
else
{
Write-Host -NoNewline "Building $( $file . Name ) without project $( $node . Node . Include ) ... "
}
}
}
else
{
continue
}
dotnet build $file . FullName > $null
if ( $LastExitCode -eq 0 )
{
Write-Output "Building succeeded."
if ( $node . Node . Version )
{
$obseletes += [ PSCustomObject ]@{
File = $file ;
Type = 'Package' ;
Name = $node . Node . Include ;
Version = $node . Node . Version ;
}
}
else
{
$obseletes += [ PSCustomObject ]@{
File = $file ;
Type = 'Project' ;
Name = $node . Node . Include ;
}
}
}
else
{
Write-Output "Building failed."
}
if ( $null -eq $previousNode )
{
$parentNode . PrependChild ( $node . Node ) > $null
}
else
{
$parentNode . InsertAfter ( $node . Node , $previousNode . Node ) > $null
}
# $xml.OuterXml
$xml . Save ( $file . FullName )
}
[ System.IO.File ]:: WriteAllBytes ( $file . FullName , $rawFileContent )
dotnet build $file . FullName > $null
if ( $LastExitCode -ne 0 )
{
Write-Error "Failed to build $( $file . FullName ) after project file restore. Was project broken before?"
return
}
}
Write-Output ""
Write-Output "-------------------------------------------------------------------------"
Write-Output "Analyse completed in $( $stopWatch . Elapsed . TotalSeconds ) seconds"
Write-Output " $( $obseletes . Length ) reference(s) could potentially be removed."
$previousFile = $null
foreach ( $obselete in $obseletes )
{
if ( $previousFile -ne $obselete . File )
{
Write-Output ""
Write-Output "Project: $( $obselete . File . Name ) "
}
if ( $obselete . Type -eq 'Package' )
{
Write-Output "Package reference: $( $obselete . Name ) ( $( $obselete . Version ) )"
}
else
{
Write-Output "Project refence: $( $obselete . Name ) "
}
$previousFile = $obselete . File
}
Summary
This was a fun script to do. It took me some hours to write this, but it was
worth it.