如何在解决方案中查找未使用的 NuGet 包?

270

如何在解决方案中查找未使用的NuGet软件包?

我有很多解决方案,安装了很多软件包,并且其中有很多被标记为需要更新。

但是,我担心会有破坏性变化,因此首先想要通过删除任何未使用的软件包来清理。


2
你意识到你并不使用的包中的破坏性更改也不会影响你...至于这个问题,我只需删除所有NuGet包,并重新添加编译器告诉我的内容。 - Ohad Schneider
9
@OhadSchneider 不是...但是我的强迫症不希望所有未使用的包的废物堆积,比如在部署中。 - SteveC
3
如果你有意不使用某些软件包的最新版本,那么这样做可能会带来问题。 - Matthew
12
对于未来的读者而言,VS2022已经内置了此选项。只需在解决方案资源管理器中右键单击任何项目,然后选择“删除未使用的引用”。这被提及在下面一个回答中,但并没有得到太多关注。 - dotNET
我有VS2022,但是我没有看到任何“删除未使用的引用”选项。版本17.2.3。 - KWallace
3
@KWallace,请查看JeeShen Lee的回答中Frank Rosario的评论。我有一个使用.NET Framework 4.8、.NET Standard 2.0和.NET 5.0项目的解决方案,但“删除未使用的引用”只适用于.NET Standard 2.0和.NET 5.0项目。我必须使用ReSharper来处理.NET Framework项目。 - Theophilus
11个回答

81

ReSharper 2016.1有一个功能,可以删除未使用的NuGet包

它可以在解决方案上运行,也可以在解决方案中每个项目上运行,并执行以下操作:

  1. 分析你的代码并收集对程序集的引用。
  2. 基于程序集的用法构建NuGet使用图形。
  3. 假定没有内容文件、自身未使用且没有使用的依赖项的包是未使用的,并建议将其删除。

不幸的是,这对于project.json项目(RSRP-454515)和ASP.NET core项目(RSRP-459076)无效。


2
我使用的是2016.1版本,但R#无法删除未使用的NuGet引用。我正在使用带有NuGet3的project.json - 这是已知问题吗? - Peter McEvoy
2
@PeterMcEvoy 是的,这是已知问题。感谢您指出。我已更新答案以澄清。 - ulex
6
我可能眼瞎了,但我没有看到任何实际运行这个工具的方法。 - claudekennilol
25
@claudekennilol 刚刚想出来了。右键点击项目,然后有一个“优化引用”的选项... - Matt Sanders
1
@bubbleking,你不应该期望每个人都能像你一样轻易支付这笔费用。根据你所在的世界地区,每年300美元可能是一笔很大的开销。 - Excludos
显示剩余8条评论

55

Visual Studio 2019 (版本16.9)内置了remove-unused-packages功能,现在我们需要手动启用它。

转到“工具”>“选项”>“文本编辑器”>“C#”>“高级”>(在分析部分下)勾选“显示‘已删除未使用的引用’命令”

Visual Studio版本16.10具有删除未使用引用的功能。右键单击项目>删除未使用的引用。

输入图像描述


3
我没有看到这个选项,你启用了其他选项来显示实验性项目吗?我正在使用VS Pro 2019,16.9.3。 - debracey
不确定VS Pro是否有此选项。我之前使用的是VS Community 16.9,现在使用的是VS Community 16.10,两个版本都显示了该选项。有使用VS Pro的人可以帮忙验证一下吗? - JeeShen Lee
3
@debracey VS 16.10版本提供了删除未使用引用的功能。右键单击项目 > 删除未使用的引用。 - JeeShen Lee
7
FYI,这个功能似乎只适用于新的csproj文件;目前旧项目没有此选项。https://github.com/dotnet/roslyn/issues/54801 - Frank Rosario
2
这只是提供让我移除任何和所有的软件包。它并没有说哪些是未使用的!Vs2022 - Daniel Williams
1
同意丹尼尔的观点 - 我刚试了一下,它删除了已使用的引用并破坏了项目... - Chris Nevill

29
您可以使用Visual Studio扩展程序ResolveUR - 解决未使用引用来解决未使用的引用,包括Visual Studio 2012/2013/2015项目中的NuGet引用,通过Solution Explorer工具窗口中的解决方案和项目节点菜单项。这并不是一件容易的事情,所以建议您在操作前进行备份和/或提交,以便在出现问题时回滚。

这个程序在删除库方面过于急躁... 当使用网站时要小心,你会发现许多DLL是必需的,但已经被删除了。如果一个NuGet包需要另一个包,即使你没有硬依赖它,也不应该删除它。 - jsgoupil
我曾使用过这个工具,但效果不佳。它删除了很多实际在使用的东西(或者至少建议删除)。有几次我相信了它的建议,结果不得不花费宝贵的时间来恢复它所删除的内容。 - Ryan VandenHeuvel
5
注意:该工具未经过测试,无法与DotNet Web项目(如Asp.Net、MVC)、Windows CE和Silverlight类型的项目一起使用。使用该工具需自行承担风险。 - Korayem
这个工具真的很好!它足够智能,即使您的代码中没有直接引用 NUnit.ConsoleRunner 等内容,也不会将其删除。 - OlegI
这个工具很慢。我有一个解决方案,包含超过250个项目... 我等啊等啊... 唉。 - Piotr Kula

24

在 Visual Studio 2019 中,右键单击 Dotnet core 项目,你将看到一个名为“Remove unused references”的选项。

输入图像描述

3
我没有这个选项。你需要做其他什么来启用它吗? - Sam Carlson
尝试更新Visual Studio。 - Sumesh Es
我正在使用最新版本 - 16.11.3 - Sam Carlson
我正在使用 Visual Studio 社区版 16.10.0。 - Sumesh Es

22

您可以使用 ReSharper 2019.1.1 来完成此操作。

右键单击项目 > 重构 > 删除未使用的引用。

如果您的项目规模较小,可以使用:项目 > 优化已使用的引用 . . .

一个窗口将弹出。选择所有引用并将它们全部删除。然后重新添加那些给您编译错误的引用。


4
在带有Resharper的VS.Net 2019中,我发现以下选项: “解决方案资源管理器 > 引用 > 优化引用...” - R. Schreurs
"优化引用工具窗口显示当前项目中未使用和已使用的程序集引用,并展示引用的具体使用方式。" https://blog.jetbrains.com/dotnet/2012/01/03/optimizing-assembly-references-with-resharper-61/ - Nick Gallimore

9
以下是一个小的PowerShell脚本,用于查找.NET Core / .NET 5+项目中多余的NuGet软件包。对于每个项目文件,它会删除每个引用并检查是否编译。这将需要很长时间。完成后,您将获得每个可能被排除的引用的摘要。最终决定哪些引用应该被删除由您来决定。由于依赖关系,您很可能无法删除所有建议的内容,但它应该为您提供一个良好的起点。
请将下面的脚本保存为ps1文件,并在第89行中将字符串C:\MySolutionDirectory替换为您要扫描的目录,然后运行ps1文件。如果出现问题,请先备份。
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
}

在这里可以找到更多信息:https://devblog.pekspro.com/posts/finding-redundant-project-references


非常好的脚本!! - Olivier Giniaux
似乎这个脚本是用来获取冗余的Nugets,但无法检测未使用的包。 - juanora

8
在 Visual Studio 2019 的最新版本和 Visual Studio 2022 中,您可以删除未使用的包,如先前评论中所述,但仅适用于 SDK 样式项目。如果您尝试在旧项目上操作,例如 .Net Framework,您将看不到此选项。作为解决方法,请验证,您可以创建两个简单的控制台应用程序:一个使用 .Net Core 或更高版本,另一个使用 .Net Framework 4.7 或 4.8。
请参考:删除未使用的引用

1

1
这是体力劳动,但是它有效。
  1. 使用ReSharper或类似的代码分析工具识别项目中未使用的引用并在相应项目中卸载nuget。

  2. 有时已卸载的nugets仍然留在“已安装包”和“更新”列表中。在关闭Visual Studio之后删除“packages”文件夹,然后重新打开解决方案并恢复你的nugets。


1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接