Nuget Package Pruning

O NuGet package pruning é o mecanismo que remove do grafo de dependências os pacotes que o próprio runtime do .NET já fornece. Com o .NET 10, esse recurso passou a vir habilitado por padrão e mudou a forma como o restore enxerga bibliotecas antigas.

Ao longo do tempo, o ecossistema NuGet acumulou pacotes que perderam a razão de existir, porque o runtime incorporou essas bibliotecas como parte da plataforma. Por isso, muitos projetos modernos ainda arrastam referências que nunca chegam a ser usadas em execução.

Segundo a telemetria da Microsoft, projetos que adotam os novos padrões do .NET 10 recebem 70% menos relatórios de vulnerabilidades transitivas. Neste artigo, você vai entender o problema, o funcionamento interno e as configurações que controlam o comportamento.

O problema que o NuGet package pruning resolve

Muitas bibliotecas publicadas no nuget.org miram o netstandard2.0 para alcançar a maior compatibilidade possível. Essa escolha faz sentido para o autor da biblioteca, mas cobra um preço de quem consome o pacote.

Para funcionar nesses alvos antigos, essas bibliotecas declaram dependências explícitas em pacotes como System.Memory, System.Text.Json e System.IO.Pipelines. No entanto, esses mesmos pacotes já fazem parte do runtime nas versões modernas do .NET.

Antes do NuGet package pruning, o restore resolvia essas dependências e as incluía no grafo mesmo quando o runtime já entregava uma versão mais recente. Na prática, o projeto carregava duas fontes para a mesma biblioteca.

A própria documentação do NuGet cita um caso típico: um System.Text.Json 8.0.0 aparece em um projeto que mira o .NET 8 ou o .NET 9. Nesse cenário, a resolução de conflitos do build descarta o assembly do pacote, porque o runtime já oferece o mesmo assembly.

Falsos positivos nas auditorias

Quando alguém publica uma CVE contra uma versão antiga, como o System.Text.Json 8.0.0, o NuGet Audit emite o alerta. Contudo, a aplicação em execução usa a versão do runtime, e não a versão do pacote.

O resultado é um relatório cheio de ruído. Assim, a equipe gasta tempo investigando vulnerabilidades que não afetam o software de verdade.

Grafos maiores e referências obsoletas

Além dos falsos positivos, pacotes desnecessários entram nos arquivos de saída do restore. Isso significa mais downloads, mais itens para resolver e mais tempo de build.

Por fim, entradas antigas permanecem no grafo mesmo quando o projeto já não depende delas de forma efetiva. Com isso, o gerenciamento de dependências fica menos claro para quem mantém o código.

Como o NuGet package pruning funciona

O NuGet package pruning atua durante o restore, antes de qualquer download ou resolução de conflito. Em outras palavras, o pacote podado nem chega a ser considerado no restante do processo.

O .NET SDK traz uma lista de pacotes fornecidos por cada target framework, junto com a versão máxima que aquele framework entrega. Essa lista nasce dos frameworks compartilhados referenciados diretamente pelo projeto.

Quando uma dependência transitiva cai dentro desse intervalo, o NuGet a remove do grafo. Por exemplo, em um projeto net10.0, o runtime fornece o System.Text.Json 10.0.x, então qualquer referência transitiva igual ou inferior a essa versão desaparece.

O raciocínio inverso também vale. Se uma biblioteca exigir uma versão mais alta do que a fornecida pelo framework alvo, o NuGet package pruning mantém o pacote, porque a plataforma não cobre aquela versão.

Dependências transitivas e diretas no NuGet package pruning

O comportamento muda conforme o tipo de dependência. Para dependências transitivas, a remoção é completa: o pacote não é baixado, não aparece nas saídas do restore e não entra no relatório do NuGet Audit.

Já para dependências diretas, declaradas no .csproj, o NuGet aplica implicitamente PrivateAssets="all" e IncludeAssets="none". Dessa forma, o build não usa os assemblies do pacote e a referência não flui para quem consome o projeto.

A linha continua no arquivo de projeto até que o desenvolvedor a remova manualmente. Por isso, o NuGet emite um aviso específico quando essa remoção é segura, como veremos adiante.

Uma tabela com todos os casos

A documentação oficial resume as regras em quatro situações. Veja a seguir esse resumo traduzido.

Situação da dependência Comportamento
Pacote transitivo trazido por outro pacote Podado
Pacote transitivo trazido por outro projeto Podado
PackageReference direto Recebe PrivateAssets="all" e IncludeAssets="none", com aviso NU1510 quando puder sair de todos os frameworks
ProjectReference com o mesmo ID Não é podado e gera o aviso NU1511

Como ligar e desligar o NuGet package pruning

Quem controla o recurso é a propriedade MSBuild RestoreEnablePackagePruning. Ela vale por target framework e aceita os valores true e false.

No .NET 9 SDK, a partir da versão 9.0.200 e do NuGet 6.13, o recurso chegou como opt-in. Nessa primeira versão, apenas pacotes transitivos eram afetados.

No .NET 10 SDK, o NuGet package pruning vem ligado por padrão para todos os frameworks de um projeto que tenha pelo menos um alvo net10.0 ou superior. Além disso, o recurso passou a tratar também as referências diretas.

Esse detalhe importa em projetos multialvo. Se qualquer target framework for net10.0 ou posterior, o pruning se aplica a todos os frameworks do projeto, não apenas ao net10.0.

Para desligar o recurso, basta definir a propriedade como false no projeto ou no Directory.Build.props:

<PropertyGroup>
  <!-- Desliga o pruning para este projeto -->
  <RestoreEnablePackagePruning>false</RestoreEnablePackagePruning>
</PropertyGroup>

Em projetos que ainda usam o .NET 9, a configuração é a oposta. A própria documentação do NuGet recomenda testar o recurso nessas versões:

<PropertyGroup>
  <!-- Ativa o pruning de forma antecipada no .NET 9 SDK -->
  <RestoreEnablePackagePruning>true</RestoreEnablePackagePruning>
</PropertyGroup>

Vale lembrar que o NuGet package pruning depende do resolvedor de dependências padrão, introduzido no NuGet 6.12. Portanto, quem força o resolvedor legado com RestoreUseLegacyDependencyResolver não recebe o benefício.

NuGetAuditMode e o NuGet package pruning

Junto com o NuGet package pruning, o .NET 10 alterou o valor padrão da propriedade NuGetAuditMode para all em projetos que miram net10.0 ou superior. Antes, o padrão era direct, que auditava apenas as dependências de topo.

Com all, o NuGet Audit passa a verificar também as dependências transitivas. À primeira vista, isso poderia gerar ainda mais alertas.

É exatamente aqui que o pruning se torna essencial. Ele tira do grafo os pacotes fornecidos pelo runtime, que seriam a maior fonte de falsos positivos, e a auditoria ampliada passa a incidir sobre um grafo enxuto.

A combinação das duas configurações explica a queda de 70% nos relatórios de vulnerabilidades transitivas. Em outras palavras, os alertas restantes tendem a apontar problemas que exigem ação.

<!-- Padrões do .NET 10 -->
<PropertyGroup>
  <NuGetAuditMode>all</NuGetAuditMode>
  <RestoreEnablePackagePruning>true</RestoreEnablePackagePruning>
</PropertyGroup>

A fonte dos dados de vulnerabilidade do nuget.org é o GitHub Advisory Database. Assim, o grafo mais limpo melhora a precisão de qualquer verificação que use essa base, inclusive em pipelines de CI.

Os avisos NU1510 e NU1511 do NuGet package pruning

O aviso NU1510 é um dos elementos mais práticos do recurso para o dia a dia. Ele surge quando uma referência direta pode sair do projeto, porque o runtime já fornece o pacote em todos os target frameworks.

O aviso só aparece quando o projeto mira net10.0 ou superior e quando a remoção do PackageReference é segura em todos os alvos. Por exemplo, um projeto apenas net10.0 com uma referência explícita a System.Text.Json recebe o NU1510.

Em um projeto multialvo com net10.0 e net48, a mesma referência não gera o aviso. Afinal, o net48 não traz o System.Text.Json como parte do framework, então a referência ainda é necessária para esse alvo.

Nesses casos, a solução recomendada é a inclusão condicional:

<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
  <!-- Referência necessária apenas no .NET Framework -->
  <PackageReference Include="System.Text.Json" Version="9.0.8" />
</ItemGroup>

O aviso NU1511, por sua vez, aparece quando um ProjectReference tem o mesmo ID de um pacote da lista de pruning. Como a poda de projetos não é suportada, a saída é renomear o projeto ou remover o PrunePackageReference que causou o conflito.

Impacto no deps.json e no nuspec

O recurso também altera os artefatos gerados pelo build. O arquivo *.deps.json, que descreve as dependências da aplicação em tempo de execução, passa a ter menos entradas.

Isso acontece porque os pacotes fornecidos pelo runtime saem automaticamente desse arquivo. Com isso, ele reflete com mais fidelidade o que a aplicação carrega de verdade.

No caso de bibliotecas publicadas como pacotes, os atributos PrivateAssets="all" e IncludeAssets="none" impedem que o pacote podável apareça no .nuspec gerado. Dessa forma, quem consome a biblioteca não herda uma dependência inútil.

A documentação mostra um exemplo claro: um projeto com alvos net9.0 e netstandard2.0 que referencia System.Text.Json. O .nuspec resultante mantém a dependência apenas no grupo do .NET Framework e deixa o grupo net9.0 vazio.

Benefícios de desempenho do NuGet package pruning

Os ganhos do NuGet package pruning vão além da segurança e da limpeza do grafo. Com menos pacotes, o restore tem menos itens para resolver, menos downloads e menos chances de conflito de versão.

Na telemetria da Microsoft, restores com o recurso habilitado apresentam uma taxa de sucesso mensuravelmente maior. Além disso, em nível de projeto, o pruning pode reduzir o tempo de restore em até 50%.

Esse ganho pesa mais em soluções grandes, com muitos projetos e dependências. Nesses casos, o tempo acumulado de restore representa uma fatia relevante do ciclo de desenvolvimento e do pipeline de CI.

Se você publica aplicações com GitHub Actions, cada segundo economizado no restore se repete em toda execução do workflow.

Exemplo prático do NuGet package pruning

Para ilustrar o impacto, considere um projeto net10.0 com duas dependências: Microsoft.Extensions.AI e NuGet.Protocol. Esse é o exemplo usado pela equipe do NuGet no anúncio oficial.

Sem pruning, o comando dotnet list package --include-transitive lista uma série de pacotes transitivos. Entre eles aparecem System.Text.Json, System.Diagnostics.DiagnosticSource, System.Threading.Channels e System.Formats.Asn1 6.0.0.

Esse último possui uma vulnerabilidade conhecida de severidade alta. Por isso, o NuGet Audit emite o aviso NU1903 durante o restore.

Com o NuGet package pruning habilitado, os pacotes fornecidos pelo runtime do net10.0 saem do grafo. Assim, o aviso de vulnerabilidade desaparece e a lista de transitivos fica bem menor.

Restam apenas os pacotes que a aplicação realmente precisa, como Newtonsoft.Json, a família NuGet.*, a família Microsoft.Extensions.* e System.Numerics.Tensors. Projetos com árvores mais profundas tendem a ver uma redução ainda maior.

Como verificar o NuGet package pruning

Para inspecionar quais pacotes saem do grafo, aumente a verbosidade do restore. Nesse modo, o NuGet exibe uma mensagem para cada pacote removido, indicando o target framework correspondente.

# Mostra as mensagens de pruning durante o restore
dotnet restore --verbosity detailed

O comando dotnet list package --include-transitive também ajuda a comparar o grafo antes e depois. No .NET 10, a forma recomendada é dotnet package list, que é um alias com o substantivo primeiro.

# Lista dependências diretas e transitivas
dotnet package list --include-transitive

# Filtra apenas pacotes com vulnerabilidades conhecidas
dotnet package list --include-transitive --vulnerable

Se o projeto usa arquivo de lock, fique atento ao próximo restore. O packages.lock.json regenerado pode listar menos pacotes do que antes, o que produz um diff maior do que o normal.

Pacotes que se tornarão podáveis no futuro

A lista de pacotes podáveis cresce à medida que o .NET evolui. Pacotes que hoje existem de forma independente podem entrar no runtime em versões futuras.

O System.IO.Pipelines é um exemplo histórico: começou como pacote autônomo e depois passou a integrar a plataforma. Conforme mais pacotes seguem esse caminho, o pruning passa a removê-los dos projetos que migram para os novos target frameworks.

Na prática, o benefício do recurso cresce a cada versão do .NET, sem exigir alterações do desenvolvedor. Para um panorama das mudanças específicas dessa versão, veja também o artigo sobre o pruning no .NET 10.

Conclusão

O NuGet package pruning representa uma evolução importante na forma como o NuGet gerencia o grafo de dependências. Ao remover pacotes que o runtime já fornece, ele elimina falsos positivos, reduz o grafo e acelera o restore.

A combinação de RestoreEnablePackagePruning=true e NuGetAuditMode=all como padrão no .NET 10 é uma decisão deliberada da equipe do NuGet. Com ela, segurança e desempenho melhoram sem configuração manual.

Para projetos ainda no .NET 9, a recomendação é ativar o NuGet package pruning de forma explícita. Assim, a equipe se adianta ao comportamento que já é padrão no .NET 10.

FAQ

O NuGet package pruning pode remover um pacote que minha aplicação realmente usa?

Não. O recurso só poda pacotes que o runtime fornece em versão igual ou superior, então a implementação continua disponível pela plataforma.

O que acontece em projetos multialvo com net48 e net10.0?

Nesse caso, o pruning vale para todos os frameworks do projeto, mas o net48 não fornece pacotes como System.Text.Json, então eles permanecem. O NU1510 também não aparece, porque a referência ainda é necessária para o net48.

Preciso atualizar o SDK para usar o package pruning?

Sim. O recurso existe como opt-in a partir do .NET SDK 9.0.200 e vem habilitado por padrão no .NET 10 SDK.

O que fazer ao receber o aviso NU1510?

Remova o PackageReference indicado no aviso. Em projetos multialvo, use uma condição para manter a referência apenas nos frameworks que precisam dela.

O package pruning afeta o deps.json da aplicação?

Sim. O arquivo *.deps.json passa a ter menos entradas, porque os pacotes fornecidos pelo runtime saem das dependências declaradas.

Posso desabilitar o pruning apenas em um framework?

Sim. A propriedade RestoreEnablePackagePruning vale por target framework, então é possível condicioná-la ao $(TargetFramework) desejado.