How can I view the hierarchy of dependencies between NuGet packages (either textual or graphically)?
15 Answers
If you're using the new .csproj, you could get all dependencies with reference in here (after project built):
{ProjectDir}\obj\project.assets.json
3 Comments
<PackageReference> format instead of a packages.config file. I have a legacy app with an oldschool csproj, modified to use PackageReference and this file exists in my obj folder.Like @neil-barnwell solution, but works with NuGet.Core 2.7+
Install-Package NuGet.Core
Here is the code
using System;
using System.Linq;
using System.Runtime.Versioning;
using System.IO;
using NuGet;
public class Program
{
public static void Main(string[] args)
{
var frameworkName = new FrameworkName(".NETFramework, Version=4.0");
// var packageSource = "https://www.nuget.org/api/v2/";
var packageSource = Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), "NuGet", "Cache");
var repository = PackageRepositoryFactory.Default.CreateRepository(packageSource);
const bool prerelease = false;
var packages = repository.GetPackages()
.Where(p => prerelease ? p.IsAbsoluteLatestVersion : p.IsLatestVersion)
.Where(p => VersionUtility.IsCompatible(frameworkName, p.GetSupportedFrameworks()));
foreach (IPackage package in packages)
{
GetValue(repository, frameworkName, package, prerelease, 0);
}
Console.WriteLine();
Console.WriteLine("Press Enter...");
Console.ReadLine();
}
private static void GetValue(IPackageRepository repository, FrameworkName frameworkName, IPackage package, bool prerelease, int level)
{
Console.WriteLine("{0}{1}", new string(' ', level * 3), package);
foreach (PackageDependency dependency in package.GetCompatiblePackageDependencies(frameworkName))
{
IPackage subPackage = repository.ResolveDependency(dependency, prerelease, true);
GetValue(repository, frameworkName, subPackage, prerelease, level + 1);
}
}
}
1 Comment
It is also possible to write code against the API in NuGet.Core. Install it via NuGet:
install-package nuget.core
Then you can get a repository object and walk the graph. Here's a sample app I just built:
using System;
using System.Collections.Generic;
using System.Linq;
using NuGet;
namespace ConsoleApplication2
{
class Program
{
static void Main()
{
var repo = new LocalPackageRepository(@"C:\Code\Common\Group\Business-Logic\packages");
IQueryable<IPackage> packages = repo.GetPackages();
OutputGraph(repo, packages, 0);
}
static void OutputGraph(LocalPackageRepository repository, IEnumerable<IPackage> packages, int depth)
{
foreach (IPackage package in packages)
{
Console.WriteLine("{0}{1} v{2}", new string(' ', depth), package.Id, package.Version);
IList<IPackage> dependentPackages = new List<IPackage>();
foreach (var dependency in package.Dependencies)
{
dependentPackages.Add(repository.FindPackage(dependency.Id, dependency.VersionSpec.ToString()));
}
OutputGraph(repository, dependentPackages, depth += 3);
}
}
}
}
In my case, this app outputs something like this:
MyCompany.Castle v1.1.0.3
Castle.Windsor v2.5.3
Castle.Core v2.5.2
MyCompany.Common v1.1.0.6
CommonServiceLocator v1.0
MyCompany.Enum v1.1.0.7
MyCompany.Common v1.1.0.6
CommonServiceLocator v1.0
MyCompany.Enum v1.1.0.7
MyCompany.Enum v1.1.0.7
MyCompany.Versioning v1.3
Castle.Core v2.5.2
Castle.Windsor v2.5.3
Castle.Core v2.5.2
CommonServiceLocator v1.0
NUnit v2.5.10.11092
RhinoMocks v3.6
7 Comments
FindPackagesI've found a nice NPM package to print the dependency tree into console. Of course if you don't mind using/installing NPM/Node.JS.
Considering other solutions, this is the most simple one, you don't need to write your own code or register something, and you get just such dependency tree as you expect. But it works only with packages.config format.
I can't believe this functionality is absent in free Visual Studio editions or nuget.exe too.
2 Comments
I Can Has .NET Core (GitHub repository) produces nice graphs of NuGet dependencies along with a Graphviz representation. And as its name implies, you also get .NET Core compatibility information for free.
If you prefer to run it locally on your computer, I Can Has .NET Core also offers a console mode.
2 Comments
I add a compatible solution with the latest version of nuget-core
install-package nuget.core
This is the console App to get the dependencies graph
class Program
{
static void Main()
{
Console.Write("Enter the local repo folder: ");
var repoFolder = Console.ReadLine();
var repo = new LocalPackageRepository(repoFolder);
IQueryable<IPackage> packages = repo.GetPackages();
OutputGraph(repo, packages, 0);
}
static void OutputGraph(LocalPackageRepository repository, IEnumerable<IPackage> packages, int depth)
{
foreach (IPackage package in packages)
{
Console.WriteLine("{0}{1} v{2}", new string(' ', depth), package.Id, package.Version);
IList<IPackage> dependentPackages = new List<IPackage>();
foreach (var dependencySet in package.DependencySets)
{
foreach (var dependency in dependencySet.Dependencies)
{
var dependentPackage = repository.FindPackage(dependency.Id, dependency.VersionSpec, true, true);
if (dependentPackage != null)
{
dependentPackages.Add(dependentPackage);
}
}
}
OutputGraph(repository, dependentPackages, depth + 3);
}
}
}
Comments
Having gone through all existing answers, IMHO they are incomplete and/or obsolete. Fortunately, it seems that today there are several good options:
- dotnet nuget why - shows the dependency graph for a particular package
- dotnet list package --include-transitive - Lists transitive packages, in addition to the top-level packages (flattened list)
- nuget-deps-tree - reads the NuGet dependencies of a .NET project, and generates a dependencies tree object (JSON or TXT)
- depends - Tool for generating dependency trees for .NET projects
Or roll your own using the modern APIs (not Nuget.Core):
private static async Task GetRecursiveDependenciesCore(string id, string version, ConcurrentQueue<string> dependencies)
{
var sourceRepository = new SourceRepository(new PackageSource(NugetOrgSource), Repository.Provider.GetCoreV3());
var dependencyResource = await sourceRepository.GetResourceAsync<DependencyInfoResource>(CancellationToken.None);
var package = await dependencyResource.ResolvePackage(new PackageIdentity(id, new NuGetVersion(version)), NuGetFramework.AnyFramework, new NugetLogger(), CancellationToken.None);
if (package == null)
{
throw new InvalidOperationException("Could not locate dependency!");
}
foreach (var dependency in package.Dependencies)
{
dependencies.Enqueue(dependency.Id + " " + dependency.VersionRange.MinVersion);
}
await Task.WhenAll(package.Dependencies.Select(d => GetRecursiveDependenciesCore(d.Id, d.VersionRange.MinVersion.ToNormalizedString(), dependencies)));
}
Comments
This is a built-in feature in the new .csproj format. In Visual Studio 2017+, open the Solution Explorer and you can find you packages like:
{Your project}->Dependencies->Packages
You can open each NuGet dependency tree and run with it recursively, effectively seeing not only the dependency tree for specific packages, but also which NuGet packages your project actually installs.
7 Comments
Package Visualizer from NuGet 1.4 should work. See http://docs.nuget.org/docs/release-notes/nuget-1.4
2 Comments
Another option you have is to use the nuget-deps-tree npm package. It supports both the packages.config format and the newer assets format used by .NET projects.
1 Comment
I built a powershell script net-pkg.psm1 to draw hierarchy of dependencies.
The script works by reading file .\obj\project.assets.json (the file when you run dotnet restore)
Assume you are in Windows OS with Powershell built-in.
Just download the file net-pkg.psm1 and run these command to see it work.
> ipmo -Force "C:\dev-note\learning\dotnet\net-pkg.psm1"
> cd E:\path\to\folder\of\csproj
> initialize-asset
> build-map 'net8.0'
> show-tree
└─ net8.0
├─ Microsoft.AspNetCore.Mvc.NewtonsoftJson/8.0.3
├─ MyOrg.Proj.Jobs/2.3.0
│ ├─ MyOrg.Proj.Persistence.CosmosDb/10.0.1
│ │ ├─ Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0
│ │ ├─ MyOrg.Proj.Persistence.Abstractions/10.0.1
│ │ │ ├─ Newtonsoft.Json/13.0.2 - 13.0.3
│ │ │ └─ MyOrg.Proj.Requests.Abstractions/6.0.0
│ │ ├─ Azure.ResourceManager.CosmosDB/1.2.1
│ │ ├─ MyOrg.Proj.ManagedIdentity.Cosmos/4.0.2
│ │ │ ├─ MyOrg.Proj.ManagedIdentity/4.0.2
│ │ │ │ └─ Microsoft.Extensions.Configuration.Abstractions/8.0.0
│ │ │ └─ Microsoft.Azure.Cosmos/3.35.4
│ │ └─ Microsoft.AspNetCore.Hosting.Abstractions/2.2.0
│ └─ MyOrg.Proj.Persistence.InMemory/10.0.1
│ ├─ MyOrg.Proj.Persistence.Abstractions/10.0.1
│ └─ MyOrg.Proj.Requests.Abstractions/6.0.0
└─ Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0
2 Comments
net-pkg.psml also need to be above, or is it a 'standard' pkg available to all? Maybe just a little more explanation to get out of low quality answer review queue.net-pkg.psm1 and run those above command to see it work.nugraph is a .NET tool I wrote that generates visual dependency graph of NuGet packages.
Install it with
dotnet tool install --global nugraph
Then run it with
nugraph Microsoft.Extensions.Logging.Console
It can generate both Mermaid and Graphviz diagrams that look like this.
It has many more options and can also generate graphs of .NET projects (csproj/fsproj/vbproj). Documentation is available on the nugraph homepage.
Disclaimer: I'm the author of this tool.
Comments
.NET has a built-in feature now:
dotnet list package --include-transitive
which can print an output as follows:
Project 'Maui.Controls.Sample.Sandbox' has the following package references
[net8.0-windows10.0.19041]:
Top-level Package Requested Resolved
> Microsoft.Extensions.DependencyInjection 8.0.0 8.0.0
Transitive Package Resolved
> libsodium 1.0.19
> LZMA-SDK 22.1.1
> Microsoft.AspNetCore.Authorization 8.0.0
> Microsoft.AspNetCore.Components 8.0.0
> Microsoft.AspNetCore.Components.Analyzers 8.0.0
> Microsoft.AspNetCore.Components.Forms 8.0.0
[..]
so unfortunately, it's not really hierarchical but it gives the dependencies.
For a better dependency hierarchy which does rely on external tools, one can enable generating of JSON lock files (packages.lock.json files) in her C# project using
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
then run dotnet restore and check contents of your packages.lock.json file:
{
"version": 1,
"dependencies": {
"net8.0-windows10.0.19041": {
"Microsoft.Extensions.DependencyInjection": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"libsodium": {
"type": "Transitive",
"resolved": "1.0.19",
"contentHash": "tupm/HViwBN6Knd/gckR+cLaJGR39GLmiU4iDMM5hp/1BoczMr8fwJhSU+3/C2V4hi9nBK/4FICRKtTLU30TCA=="
},
"LZMA-SDK": {
"type": "Transitive",
"resolved": "22.1.1",
"contentHash": "lxG8/H53OCqebnDFlTStwzbgtmUN/DqnXN8kffa/ctHcSEHhIPDyEa3jLzpZxVX5F8bmJgvb1mna6gnhetykIw=="
},
"Microsoft.AspNetCore.Authorization": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "OGIGJMnlWvQgcweHcv1Mq/P24Zx/brUHeEdD05NzqkSXmQSnFomTvVyCuBtCXT4JPfv2m70y1RSocmd9bIbJRg==",
"dependencies": {
"Microsoft.AspNetCore.Metadata": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0"
}
},
[..]
https://github.com/NuGet/Home/issues/6188 is unfortunately still open.
Comments
Until IDE's provide better graphing support for showing Nuget libs of your project(s), it would be practical to just have a solution that does not require any additional tooling in dotnet or similar and which works with both .NET Framework and .NET projects.
Using Powershell you can:
- Analyze the Json contents in
project.assets.jsonfiles in subdir(s) - List up the dependencies in a mermaid js graph (SVG)
- Add pan/zoom capabilities using Vanilla JS
- Write a temporary HTML file that is launched into browser
- Add tables showing top level and transitive dependencies and add filtering
Screenshot of the output the Powershell script provides
No requirement of any special dotnet tool
Works with both .NET Framework and .NET Core projects
Can show multiple projects, however, there are limitations in mermaid restricting the number of nodes and edges allowed. Recommended usage is to use one project at a time
Compile the project and the
project.assets.jsonfile should then be in theobjsubfolder of the project you want to inspect of your .NET (Framework) project source codeUsage:
Run the Powershell script from Powershell ISE or Powershell
Make sure you are in the right folder when you run the script, there should be subdir(s) with
project.assets.jsonThe function
Show-NugetDependencyGraphcan be put into your $profile file in Powershell for easier usage later on.
function Show-NugetDependencyGraph {
[CmdletBinding()]
param ()
$tempHtmlPath = [System.IO.Path]::GetTempFileName() + ".html"
$assetFiles = Get-ChildItem -Recurse -Filter "project.assets.json"
$script:mermaidGraph = @"
<!DOCTYPE html>
<html>
<head>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
<style>
body {
font-family: sans-serif;
padding: 20px;
}
.mermaid {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
height: auto;
max-height: 80vh;
overflow: auto;
}
.mermaid svg {
min-height: 800px;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<h2>NuGet Dependency Graph (Max Depth: 3)</h2>
<div class="mermaid">
graph TD
"@
$visited = @{}
$nodes = @{}
$edges = @{}
$topLevelDeps = @{}
$transitiveDeps = @{}
function Escape-MermaidLabel {
param ([string]$text)
$text = $text -replace '\(', '('
$text = $text -replace '\)', ')'
$text = $text -replace '\[', '['
$text = $text -replace '\]', ']'
$text = $text -replace ',', ','
return $text
}
function Normalize-NodeId {
param ([string]$text)
return ($text -replace '[^a-zA-Z0-9_]', '_')
}
function Add-Dependencies {
param (
[string]$pkgName,
[object]$targets,
[int]$depth,
[string]$path = ""
)
if ($depth -gt 3 -or $visited.ContainsKey($pkgName)) { return }
$visited[$pkgName] = $true
$pkgVersion = $pkgName.Split('/')[1]
$pkgId = $pkgName.Split('/')[0]
$escapedVersion = Escape-MermaidLabel($pkgVersion)
$nodeId = Normalize-NodeId ("{0}_{1}" -f $pkgId, $pkgVersion)
$nodeLabel = "$nodeId[""$pkgId<br/>v$escapedVersion""]:::level$depth"
if (-not $nodes.ContainsKey($nodeId)) {
$script:mermaidGraph += "$nodeLabel`n"
$nodes[$nodeId] = $true
}
$currentPath = if ($path) { "$path → $pkgId ($pkgVersion)" } else { "$pkgId ($pkgVersion)" }
if ($depth -eq 1) {
$topLevelDeps["$pkgId/$pkgVersion"] = $currentPath
} else {
$transitiveDeps["$pkgId/$pkgVersion"] = $currentPath
}
foreach ($target in $targets.PSObject.Properties) {
$pkg = $target.Value.$pkgName
if ($pkg -and $pkg.dependencies) {
foreach ($dep in $pkg.dependencies.PSObject.Properties) {
$depName = $dep.Name
$depVersion = $dep.Value
$escapedDepVersion = Escape-MermaidLabel($depVersion)
$depNodeId = Normalize-NodeId ("{0}_{1}" -f $depName, $depVersion)
$depNodeLabel = "$depNodeId[""$depName<br/>v$escapedDepVersion""]:::level$($depth+1)"
if (-not $nodes.ContainsKey($depNodeId)) {
$script:mermaidGraph += "$depNodeLabel`n"
$nodes[$depNodeId] = $true
}
$edge = "$nodeId --> $depNodeId"
if (-not $edges.ContainsKey($edge)) {
$script:mermaidGraph += "$edge`n"
$edges[$edge] = $true
}
Add-Dependencies ("$depName/$depVersion") $targets ($depth + 1) $currentPath
}
}
}
}
foreach ($file in $assetFiles) {
$json = Get-Content $file.FullName | ConvertFrom-Json
$targets = $json.targets
foreach ($target in $targets.PSObject.Properties) {
$targetPackages = $target.Value
foreach ($package in $targetPackages.PSObject.Properties) {
Add-Dependencies $package.Name $targets 1
}
}
}
$script:mermaidGraph += @"
classDef level1 fill:#cce5ff,stroke:#004085,stroke-width:2px;
classDef level2 fill:#d4edda,stroke:#155724,stroke-width:1.5px;
classDef level3 fill:#fff3cd,stroke:#856404,stroke-width:1px;
</div>
<script>
function enablePanZoom(svg) {
let isPanning = false;
let startX, startY;
let viewBox = svg.viewBox.baseVal;
let zoomFactor = 1.1;
// Initial zoom: scale to 200%
const initialZoom = 2.0;
const newWidth = viewBox.width / initialZoom;
const newHeight = viewBox.height / initialZoom;
viewBox.x += (viewBox.width - newWidth) / 2;
viewBox.y += (viewBox.height - newHeight) / 2;
viewBox.width = newWidth;
viewBox.height = newHeight;
svg.addEventListener("mousedown", (e) => {
isPanning = true;
startX = e.clientX;
startY = e.clientY;
svg.style.cursor = "grabbing";
});
svg.addEventListener("mousemove", (e) => {
if (!isPanning) return;
const dx = (e.clientX - startX) * (viewBox.width / svg.clientWidth);
const dy = (e.clientY - startY) * (viewBox.height / svg.clientHeight);
viewBox.x -= dx;
viewBox.y -= dy;
startX = e.clientX;
startY = e.clientY;
});
svg.addEventListener("mouseup", () => {
isPanning = false;
svg.style.cursor = "grab";
});
svg.addEventListener("mouseleave", () => {
isPanning = false;
svg.style.cursor = "grab";
});
svg.addEventListener("wheel", (e) => {
e.preventDefault();
const { x, y, width, height } = viewBox;
const mx = e.offsetX / svg.clientWidth;
const my = e.offsetY / svg.clientHeight;
const zoom = e.deltaY < 0 ? 1 / zoomFactor : zoomFactor;
const newWidth = width * zoom;
const newHeight = height * zoom;
viewBox.x += (width - newWidth) * mx;
viewBox.y += (height - newHeight) * my;
viewBox.width = newWidth;
viewBox.height = newHeight;
});
svg.style.cursor = "grab";
}
document.addEventListener("DOMContentLoaded", () => {
setTimeout(() => {
const svg = document.querySelector(".mermaid svg");
if (svg) {
enablePanZoom(svg);
} else {
console.warn("SVG not found after 1.5s.");
}
}, 1500);
});
</script>
<h3>🔎 Filter Dependencies</h3>
<input type="text" id="searchInput" onkeyup="filterTables()" placeholder="Search for NuGet package..." style="width: 100%; padding: 8px; margin-bottom: 20px; font-size: 16px;">
<style>
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 40px;
font-size: 14px;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #e2f0fb;
}
th {
background-color: #007bff;
color: white;
}
</style>
<h3>📦 Top-Level Dependencies</h3>
<table id="topTable">
<thead><tr><th>Package</th><th>Dependency Path</th></tr></thead>
<tbody>
"@
$sortedTopLevel = $topLevelDeps.GetEnumerator() | Sort-Object Name
foreach ($dep in $sortedTopLevel) {
$script:mermaidGraph += "<tr><td>$($dep.Key)</td><td>$($dep.Value)</td></tr>`n"
}
$script:mermaidGraph += @"
</tbody>
</table>
<h3>📚 Transitive Dependencies</h3>
<table id="transitiveTable">
<thead><tr><th>Package</th><th>Dependency Path</th></tr></thead>
<tbody>
"@
$sortedTransitive = $transitiveDeps.GetEnumerator() | Sort-Object Name
foreach ($dep in $sortedTransitive) {
$script:mermaidGraph += "<tr><td>$($dep.Key)</td><td>$($dep.Value)</td></tr>`n"
}
$script:mermaidGraph += @"
</tbody>
</table>
<script>
function filterTables() {
const input = document.getElementById('searchInput').value.toLowerCase();
['topTable', 'transitiveTable'].forEach(id => {
const rows = document.getElementById(id).getElementsByTagName('tr');
for (let i = 1; i < rows.length; i++) {
const cells = rows[i].getElementsByTagName('td');
const match = Array.from(cells).some(cell => cell.textContent.toLowerCase().includes(input));
rows[i].style.display = match ? '' : 'none';
}
});
}
</script>
</body>
</html>
"@
[System.IO.File]::WriteAllText($tempHtmlPath, $script:mermaidGraph, [System.Text.Encoding]::UTF8)
Start-Process $tempHtmlPath
}
# Run the function
Show-NugetDependencyGraph




