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.