For .NET NuGet dependency auditing, a smaller package count is not automatically safer, and a vulnerability warning is not a reason to upgrade every dependency blindly. The useful objective is a dependency graph that can be explained, a security decision that can be reproduced, and a removal that preserves the application's build and runtime contracts. A project may never call a package directly but still require it through a top-level dependency. Another project may explicitly reference an assembly that its target framework or another package already supplies. Both situations require evidence rather than guesses based on names.

This guide divides the work into inventory, causality, remediation, and verification. The example uses an isolated .NET 10 test project and does not connect to real accounts, customer data, or a live environment. The goal is not to produce an impressive before-and-after package count. It is to make every direct reference defensible, every transitive reference traceable, and every update or removal testable with fixed commands and observable pass conditions.

The direct answer to NuGet dependency auditing and pruning

For .NET NuGet dependency auditing, start by pinning the SDK and restore configuration, list both top-level and transitive dependencies, and use dotnet nuget why to identify each inclusion path. Remediate advisories against the resolved graph, then require restore, build, tests, and deployment-output checks before accepting any removal.

Package pruning is therefore not the act of deleting every PackageReference whose namespace is absent from a text search. A package may participate through MSBuild targets, source generators, configuration binding, reflection, or runtime loading. Conversely, a direct reference may be redundant because an upstream package already supplies the required dependency. The first question is “Why is this package present?” Only after answering that should a team ask, “Which layer should own its version?”

Four evidence sets are useful. Project files and central version files identify explicit declarations. The resolved assets graph identifies what restore actually selected. NuGet Audit reports advisories known to the configured audit sources at that point in time. Build, test, and publish outputs show what the artifact needs under a specific target framework and runtime choice. None of these sets is a substitute for the others.

Command syntax also depends on the installed SDK. With .NET 10, the noun-first form is dotnet package list. The official CLI reference says .NET 9 and earlier use dotnet list package. It also states that --include-transitive is not the default. An automation job should check the SDK first and select the supported form instead of treating a syntax mismatch as a project failure.

Separate top-level references, transitive dependencies, and runtime files

A top-level dependency is explicitly declared by the project. A transitive dependency is introduced because a top-level package needs it. A publish output is the file set resolved for a target framework and, when specified, a runtime identifier. These views overlap, but they do not answer the same question.

Suppose package A depends on a logging abstraction, and that abstraction or a related logging implementation depends on dependency-injection components. The application may not declare the injection package directly, but restore can still place it in the transitive graph. Its presence is not proof of an obsolete DLL. It is the result of a dependency path. dotnet nuget why exposes that path and provides much stronger evidence than filename inspection.

The inverse case is an explicit PackageReference for which code search, compiler references, configuration, targets, and tests show no purpose. If removing that one declaration leaves the expected output and behavior intact, it becomes a credible pruning candidate. Searching for using directives is only a first filter. Fully qualified names, reflection strings, serializers, analyzers, build targets, and generators can work without a conventional using directive. The compiler, automated tests, and a clean output comparison must participate in the decision.

Teams should also avoid permanently promoting every transitive dependency to a direct one. When an advisory affects a transitive package, Microsoft's NuGet guidance recommends first updating the package closest to the direct reference. If that does not resolve the advisory, the team can evaluate a nearer intermediate update, a direct reference to a fixed version, or central transitive pinning. A direct pin can be valid, but it transfers version ownership to the application. Record why it exists and the condition under which it should be removed.

Plugin and separately deployed components need an additional output check. A shared contract supplied by the host may need exclusion from the plugin directory, while a new transitive assembly introduced by an upgrade may need inclusion. A successful build on a developer machine does not prove that an isolated artifact is complete. Test the output from a clean process so that an unrelated working directory, global cache, or previously copied assembly cannot hide a missing file.

This distinction also improves review quality. A pull request that says “removed unused packages” is too vague. A useful review states whether each removed item was top-level or transitive, how its inclusion path was identified, which usage surfaces were checked, what changed in the resolved graph, and which runtime behavior was exercised after removal.

Use NuGet Audit to remediate vulnerabilities, not silence warnings

NuGet Audit evaluates packages during restore against vulnerability information from configured audit sources. The official documentation maps NU1901 to low severity, NU1902 to moderate, NU1903 to high, and NU1904 to critical. NU1900 means vulnerability information could not be obtained because of a communication problem. NU1905 indicates that an audit source does not provide vulnerability data. These conditions must not be collapsed into a single “audit passed” state.

NuGetAuditMode accepts direct or all. Microsoft's current documentation says the default is all for a project targeting net10.0 or later, while earlier target frameworks default to direct. In a multi-targeted project, one target selecting all causes audit to use that mode for all target frameworks. A repository can set the mode explicitly so that its policy remains readable and does not silently change when SDK defaults evolve.

This is a configuration excerpt; the reproduction section provides a complete executable project.


  
    net10.0
    true
    all
    moderate
  

When a direct package has an advisory, read the advisory's affected versions and usage conditions before selecting a fixed version. Update the direct reference, inspect release notes for compatibility changes, and run the relevant regression tests. When a transitive dependency is affected, first use dotnet nuget why to identify the path, then try an update at the nearest top-level package. This approach lets the package author maintain its intended dependency constraints.

Do not add broad NoWarn entries merely because a warning interrupts a build. NuGet supports NuGetAuditSuppress for a specific advisory, but Microsoft's guidance describes suppression as a last resort. A justified suppression should identify the exact advisory, explain why the affected path is unreachable or why the risk is temporarily accepted, assign an owner, and include a review date. It should not hide unrelated advisories for the same package family.

A report containing no vulnerable package entries is time-bound evidence, not a permanent safety guarantee. Advisory databases can change after the restore. An audit endpoint can be unreachable. A different target framework can resolve a different graph. Retain the timestamp, SDK version, resolved package versions, and source health with the decision. CI should distinguish an empty vulnerable-package result from NU1900 or NU1905.

Audit policy should also reflect delivery risk. A team might keep low and moderate findings visible while failing a dedicated audit workflow on high and critical findings. The official documentation shows that warning treatment can be conditioned for an audit pipeline. Whatever threshold is chosen, it should be explicit, reviewed, and consistent across projects rather than hidden in one developer's machine configuration.

A safe decision workflow for package removal

Prune dependencies in small, reversible changes. Removing many references at once makes a failed build difficult to diagnose and can mix unrelated compatibility changes. For each candidate, capture whether it is top-level or transitive, its current inclusion path, code and build-time usage, runtime role, output impact, advisory status, and rollback point.

  1. Create a baseline. Pin the SDK, target framework, and NuGet configuration. Run a clean restore, Release build, tests, and an isolated publish. Save the JSON dependency graph and an artifact filename inventory.
  2. Establish causality. Run dotnet package list --include-transitive, classify top-level and transitive entries, and run dotnet nuget why for questionable packages. Inspect source, project files, imported targets, generators, configuration, and tests.
  3. Change one decision at a time. For a vulnerability, prefer updating the nearest top-level package. For a redundant direct reference, remove one declaration and restore again. Do not combine framework migration, deployment redesign, and broad package deletion in one experiment.
  4. Compare observable outcomes. Require restore, build, and relevant tests to pass. Verify that required assemblies remain, the candidate disappears when expected, and no new NU190x condition appears. Start plugins or tools from an isolated output where applicable.
  5. Document and preserve rollback. Record the before-and-after graph, advisory links, rationale, test output, and rollback commit. If evidence is incomplete, keep the reference and create a follow-up instead of converting uncertainty into a smaller count.

This workflow prevents two common mistakes. The first is assuming that a capability included by the target framework always makes a PackageReference redundant. The correct answer depends on the target, compile assets, and deployment model. The second is assuming that two similarly named assemblies must conflict or that only one can be valid. An upgrade can replace a dependency family or add a differently named transitive component. The resolved graph and actual load behavior are more reliable than name similarity.

For automated comparison, prefer JSON over localized console text. --format json --output-version 1 fixes the current report structure and makes top-level versus transitive classification machine-readable. Store resolved versions and audit outcomes, but do not publish private feed addresses, sensitive authorization data, or internal repository metadata. A public report can state the verification method without exposing environment-specific configuration.

The package count itself should remain a secondary metric. Removing a direct reference that was already satisfied transitively may not reduce the final DLL count. Updating one package may add a legitimate cryptography or abstractions dependency. The successful outcome is a smaller unexplained surface and a better-tested artifact, not an arbitrary numerical target.

Verification and reproduction in an isolated .NET 10 project

These checks were executed in the scheduler environment with .NET SDK 10.0.400. They created only a local sandbox project and restored from a public NuGet source. The run confirmed that JSON separates top-level and transitive packages, the vulnerability query completed without a listed vulnerable package for this synthetic graph, and dotnet nuget why displayed the expected inclusion chain.

Save these four complete files in the same empty directory. global.json pins the SDK, and NuGet.Config selects public package and audit sources explicitly. Install .NET SDK 10.0.400 and ensure NuGet network access first.

NuGetAuditLab.csproj


  
    Exe
    net10.0
    true
    all
    moderate
  
  
    
  

Program.cs

System.Console.WriteLine("NuGetAuditLab ready");

global.json

{"sdk":{"version":"10.0.400","rollForward":"disable"}}

NuGet.Config



  
    
    
  
  
    
    
  

  1. Create all four files shown above. PackageReference belongs in ItemGroup, not PropertyGroup.
  2. Run dotnet --version, requiring exactly 10.0.400 for this reproduction, then run dotnet restore NuGetAuditLab.csproj --force --no-cache. A different SDK, a restore failure, or an unresolved NU190x condition fails this fixed-version check.
  3. Run dotnet package list --project NuGetAuditLab.csproj --include-transitive --format json --output-version 1. Pass only if Microsoft.Extensions.Http 10.0.0 appears in topLevelPackages and Microsoft.Extensions.DependencyInjection 10.0.0 appears in transitivePackages.
  4. Run the same list command with --vulnerable. The observed run returned exit code zero and no vulnerable-package entries for the test project. A source error, invalid JSON, listed vulnerability, or nonzero exit is a failure.
  5. Run dotnet nuget why NuGetAuditLab.csproj Microsoft.Extensions.DependencyInjection. Pass only if the path includes Microsoft.Extensions.Http, then Microsoft.Extensions.Logging, then Microsoft.Extensions.DependencyInjection.
dotnet --version
dotnet restore NuGetAuditLab.csproj --force --no-cache
dotnet package list --project NuGetAuditLab.csproj --include-transitive --format json --output-version 1
dotnet package list --project NuGetAuditLab.csproj --include-transitive --vulnerable --format json --output-version 1
dotnet nuget why NuGetAuditLab.csproj Microsoft.Extensions.DependencyInjection

The run validates CLI behavior and this synthetic dependency graph only. It does not claim that another application is secure, and it provides no invented performance figures. Readers reproducing the procedure should retain the complete output and confirm audit-source health. On .NET 9 or earlier, use the verb-first dotnet list package form documented by Microsoft rather than copying the .NET 10 command order unchanged.

A stronger project-specific test would add a before-and-after publish comparison and exercise the feature that previously consumed the candidate package. That extension depends on the project's contract and cannot be inferred from a generic package graph. The important rule is to define the observable behavior before removal, not after a failure reveals what was missing.

Conclusion: govern dependencies as an explicit contract

Effective NuGet governance is neither “always upgrade everything” nor “minimize PackageReference at all costs.” It requires a clear reason for every top-level dependency, a traceable path for every transitive package, a defensible response to each advisory, and observable conditions that must remain true after an update or removal.

The practical starting point is one reproducible baseline: pin the SDK, export the dependency graph as JSON, use dotnet nuget why on uncertain entries, enable NuGet Audit for transitive dependencies, and change one package decision at a time. Restore, build, tests, and isolated output checks then turn pruning from filename guesswork into an auditable engineering change.

This approach also improves future maintenance. When a new advisory appears, the team can identify ownership quickly. When an SDK changes command behavior or audit defaults, the repository policy remains explicit. When a deployment artifact changes, reviewers can connect the file difference to a known dependency path. The result is not merely fewer packages; it is a dependency boundary that can be reviewed, reproduced, and safely evolved.

Editorial recheck, 2026-09-27: The following summarizes verified outcomes; it is not a verbatim CLI transcript. Also run dotnet build NuGetAuditLab.csproj -c Release --no-restore and dotnet run --project NuGetAuditLab.csproj -c Release --no-build. Require exit code zero from both and the fixed program message.

SDK: 10.0.400
Top-level: Microsoft.Extensions.Http 10.0.0
Transitive: Microsoft.Extensions.DependencyInjection 10.0.0
Path: Microsoft.Extensions.Http -> Microsoft.Extensions.Logging -> Microsoft.Extensions.DependencyInjection
Vulnerable-package entries: none in this run
Program output: NuGetAuditLab ready

Official references