Forem

Cover image for NuGet Central Package Management: como criei uma dotnet tool pra limpar o lixo que ninguém limpa
Alberto Monteiro
Alberto Monteiro

Posted on

NuGet Central Package Management: como criei uma dotnet tool pra limpar o lixo que ninguém limpa

Fala galera, tudo beleza?!

Se você trabalha com .NET e tem uma solution com mais do que meia dúzia de projetos, eu tenho certeza que já passou por aquele momento: você precisa atualizar um pacote NuGet e descobre que ele está numa versão no projeto A, outra versão no projeto B, e uma terceira versão no projeto C. Já tive muita dor de cabeça com isso. E olha que eu gosto de facilitar minha vida.

Foi aí que o Central Package Management (CPM) do NuGet mudou o jogo pra mim. Mas como nem tudo são flores, ao usar o CPM no dia a dia, percebi que ele cria um probleminha silencioso que ninguém fala. E eu não fui o único — tem uma issue aberta no repositório do NuGet sobre exatamente isso, e o time basicamente disse que não vai resolver.

Então eu fui lá e fiz.

Spoiler: ficou simples, prático e resolve uma dor real. Bora lá!


CPM do NuGet, ou: por que você deveria estar usando isso ontem

Se você ainda não conhece o Central Package Management, deixa eu te explicar rapidinho. A ideia é simples: ao invés de cada .csproj da sua solution declarar a versão dos pacotes NuGet que usa, você centraliza todas as versões num único arquivo chamado Directory.Packages.props na raiz do repositório.

Nos seus projetos, o PackageReference fica assim:

<!-- No .csproj — sem Version! -->
<PackageReference Include="Newtonsoft.Json" />
Enter fullscreen mode Exit fullscreen mode

E no Directory.Packages.props:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

Simples assim. Um arquivo, uma versão, todos os projetos alinhados.

Os benefícios são enormes:

  • Uma única fonte de verdade. Acabou aquela história de "no projeto X tá na versão 6, no projeto Y tá na 7". Tudo num lugar só.
  • Upgrade de pacotes sem sofrimento. Precisa atualizar o Entity Framework em 30 projetos? Muda uma linha. Uma. Linha.
  • Menos conflito de merge. Sabe quando dois devs em branches diferentes atualizam pacotes e o merge vira um pesadelo? Com CPM a mudança de versão fica concentrada num arquivo só, os conflitos diminuem drasticamente.
  • Projetos mais limpos. Os .csproj ficam enxutos, sem aquele monte de Version="x.y.z" espalhado.
  • Segurança da supply chain. Combinando CPM com Package Source Mapping, você controla de qual feed cada pacote pode vir. Pacotes internos só do seu feed privado, pacotes públicos só do nuget.org. E com lock files (packages.lock.json), o resultado é um grafo de dependências determinístico e auditável.
  • Pinning de dependências transitivas. Aquela dependência transitiva que veio com uma vulnerabilidade? Com o transitive pinning do CPM, você força a versão corrigida sem precisar ficar caçando de qual pacote ela veio.

Se você tem uma solution com mais de 3 ou 4 projetos, CPM é praticamente obrigatório. É mágico, é lindo!!!


O problema que ninguém fala, ou: o lixo silencioso no Directory.Packages.props

Beleza, CPM é maravilhoso. Mas tem um detalhe que me incomodava faz tempo.

No dia a dia do desenvolvimento, pacotes vão e vêm. Você adiciona um pacote pra testar algo, remove depois. Refatora um projeto e tira uma dependência. Troca uma lib por outra. O que acontece? O PackageReference sai do .csproj, mas a declaração de <PackageVersion> continua lá no Directory.Packages.props, sossegada, juntando poeira.

Com o tempo, aquele arquivo que deveria ser a sua fonte de verdade vira um cemitério de referências que ninguém mais usa. Num projeto grande, eu já vi Directory.Packages.props com dezenas de entradas órfãs. Isso gera confusão: alguém novo no time olha e pensa que o pacote é usado, tenta entender onde, perde tempo. Ou pior, na hora de fazer upgrade, você atualiza versão de pacote que nem está sendo usado.

O detalhe que me fez criar a ferramenta: o Dependabot enlouquecendo

E aqui vem a parte que dói de verdade. Se você usa Dependabot (ou qualquer ferramenta de atualização automática de dependências), ele vai ler o Directory.Packages.props e criar pull requests pra atualizar todos os pacotes listados ali — inclusive os que ninguém mais usa. Ou seja, o time recebe PRs, revisa, aprova, faz merge... tudo pra atualizar versão de pacote que nem está no código. É trabalho jogado fora, é ruído no workflow, é tempo que podia estar indo pra coisa que importa.

A issue no NuGet, ou: "não vamos resolver isso"

Eu não fui o primeiro a perceber. Existe a issue #13562 no repositório do NuGet que descreve exatamente esse problema: ao desinstalar um pacote, ele sai do .csproj mas fica no Directory.Packages.props. A resposta do time do NuGet? Que isso é intencional por design — porque no contexto de um projeto individual, o NuGet não tem como saber quantos outros projetos usam aquele Directory.Packages.props. Faz sentido do ponto de vista deles, mas o resultado prático é que a sujeira vai acumulando e ninguém limpa.

A sugestão que ficou foi que talvez no futuro isso virasse uma opção opt-in, um argumento de linha de comando ou uma opção na UI. Mas pelo que eu entendi, não está no radar de prioridades do time.

Então eu pensei: se o time do NuGet não vai resolver, eu resolvo.


Nasce o nuget-cpm-cleaner, ou: resolvendo em minutos o que levaria horas na mão

A ideia é direta: a ferramenta lê o Directory.Packages.props, escaneia todos os .csproj do repositório, e descobre quais pacotes estão declarados mas não são usados por ninguém. Sem mais delongas, o fluxo funciona assim:

  1. Localiza o Directory.Packages.props — sobe a árvore de diretórios a partir da raiz que você indicou
  2. Parseia os pacotes declarados — extrai todos os <PackageVersion> do arquivo
  3. Escaneia os projetos — roda dotnet msbuild -getItem:PackageReference em cada .csproj pra saber quais pacotes são realmente usados
  4. Calcula a diferença — declarados menos usados = lixo
  5. Te deixa escolher — ou mostra um prompt interativo pra você selecionar o que remover, ou remove tudo automaticamente

O detalhe que me custou um bom tempo de pesquisa

Para descobrir quais pacotes cada projeto realmente usa, eu poderia simplesmente parsear o XML dos .csproj procurando PackageReference. Mas isso não pega tudo — pacotes podem vir de Directory.Build.props, de imports condicionais, de targets dinâmicos. A solução robusta foi usar o próprio MSBuild pra avaliar o projeto:

dotnet msbuild -getItem:PackageReference
Enter fullscreen mode Exit fullscreen mode

Esse comando retorna um JSON estruturado com todos os PackageReference resolvidos do projeto, considerando todas as importações e condições. É o MSBuild te dizendo "olha, no final das contas, esses são os pacotes que esse projeto precisa". Muito mais confiável do que parsear XML na mão.


Instalação e uso, ou: duas linhas e já era

Instala como global tool:

dotnet tool install -g nuget-cpm-cleaner
Enter fullscreen mode Exit fullscreen mode

Modo interativo — ele mostra os pacotes não usados e você escolhe o que remover:

nuget-cpm-cleaner --root C:/repos/minha-solution
Enter fullscreen mode Exit fullscreen mode

Modo automático — remove tudo sem perguntar (ótimo pra CI ou quando você confia no resultado):

nuget-cpm-cleaner --root . --auto-remove
Enter fullscreen mode Exit fullscreen mode

A saída é bem clara:

Found C:\repos\minha-solution\Directory.Packages.props
Declared packages: 42
Referenced packages across .csproj files: 38

Found 4 unused package(s):

? Select packages to remove: (Press <space> to toggle, <enter> to confirm)
> [ ] Deprecated.Package
  [ ] OldLibrary.Core
  [ ] SomeUnused.Tool
  [ ] UnusedAnalyzer

Done. Removed 2 package(s) from Directory.Packages.props:
  - Deprecated.Package
  - OldLibrary.Core
Enter fullscreen mode Exit fullscreen mode

Simples assim. E de bônus: se você rodar isso antes de configurar o Dependabot (ou como step periódico no CI), nunca mais vai receber PR de atualização de pacote fantasma.


O que ficou de aprendizado

  • CPM não é opcional em solutions grandes. Se você tem mais de 3 projetos e ainda gerencia versões de pacote em cada .csproj, está acumulando dívida técnica todo dia. Centraliza isso.

  • Ferramentas boas resolvem dores pequenas que acumulam. Um <PackageVersion> órfão não quebra nada. Mas 30 deles deixam seu arquivo de configuração sujo, confuso, e fazem o Dependabot trabalhar à toa.

  • Se a ferramenta oficial não resolve, resolve você. A issue tá aberta, o time do NuGet explicou por que não vai tratar por padrão. Em vez de esperar, criei uma tool que resolve em dois comandos. Open source é isso.

  • Use o MSBuild como fonte de verdade, não o XML cru. Parsear .csproj direto é tentador, mas o resultado real de um build depende de condições, imports, e targets que só o MSBuild resolve. O -getItem é a forma correta.

  • Dotnet tools são subestimadas. Criar uma CLI distribuível via NuGet é absurdamente fácil no .NET. PackAsTool, ToolCommandName, e pronto — qualquer dev instala com uma linha.


Para saber mais

👉 Código fonte: https://github.com/AlbertoMonteiro/nuget-cpm-cleaner

Vou ficando por aqui. Se você já usa CPM e tem aquele Directory.Packages.props precisando de uma faxina, experimenta a tool e me conta o que achou. E se você ainda não usa CPM... bom, agora não tem mais desculpa!!!

Fique à vontade para comentar, vai ser muito legal trocar uma ideia!!!

Um grande abraço!

#dotnet #nuget #cpm #centralpackagemanagement #dotnettools #csharp #opensource #dependabot

Top comments (0)