Directory.Build.props: o que é e como usar no .NET

O Directory.Build.props é um arquivo do MSBuild que define propriedades e referências de pacotes uma única vez para todos os projetos abaixo dele na árvore de pastas. Com ele, você elimina repetições nos arquivos .csproj e mantém a solução inteira com as mesmas regras de compilação.
Neste artigo, você vai entender como o MSBuild encontra esse arquivo e em que momento ele é importado. Além disso, vamos ver exemplos práticos, os riscos de aninhar arquivos e as ferramentas para depurar o resultado do build.
O que é o Directory.Build.props
Em soluções com muitos projetos, é comum repetir as mesmas propriedades em cada .csproj. Por exemplo, Nullable, ImplicitUsings, LangVersion e TreatWarningsAsErrors costumam aparecer em todos os arquivos.
Essa repetição parece inofensiva no começo. No entanto, ela gera inconsistências quando alguém altera um projeto e esquece dos outros.
O Directory.Build.props resolve esse problema de forma simples. Você cria um único arquivo na raiz da solução e todos os projetos abaixo dele herdam as configurações.
Veja o exemplo mais curto possível:
<Project>
<PropertyGroup>
<!-- Habilita nullable reference types em todos os projetos -->
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Basta colocar esse arquivo ao lado do .sln para que todos os projetos passem a usar Nullable habilitado. Dessa forma, você não precisa tocar em nenhum .csproj.
Na prática, o Directory.Build.props funciona como um conjunto de valores padrão para a solução. Cada projeto continua livre para sobrescrever o que precisar.
Também existe um atalho na CLI do .NET para criar o arquivo. O comando dotnet new buildprops, executado na raiz do repositório, gera um Directory.Build.props vazio e pronto para editar.
Como o MSBuild encontra o Directory.Build.props
O MSBuild localiza o arquivo sozinho, sem nenhuma etapa de configuração. Isso acontece porque os projetos no formato SDK, como os que usam Microsoft.NET.Sdk, importam uma lógica padrão logo no início da avaliação.
Segundo a documentação oficial do MSBuild, o arquivo Microsoft.Common.props procura o Directory.Build.props na estrutura de pastas. A busca começa no diretório do projeto e sobe nível por nível.
Quando encontra o primeiro arquivo, o MSBuild para de procurar. Ou seja, ele importa exatamente um Directory.Build.props e não continua subindo para mesclar outras cópias.
Um detalhe importante é que a localização do arquivo .sln não influencia essa busca. O que importa é a posição do .csproj na árvore de pastas.
Por isso, o arquivo pode ficar na raiz do repositório e valer para várias soluções ao mesmo tempo. Também pode ficar dentro de uma subpasta e valer apenas para os projetos dela.
Ordem de importação e sobrescrita
A importação acontece cedo, antes de o MSBuild avaliar os elementos <PropertyGroup> do próprio projeto. Como consequência, qualquer propriedade definida depois no .csproj vence.
Imagine que o arquivo da raiz define Nullable como enable. Se um único projeto definir Nullable como disable, esse projeto fica com disable, enquanto os demais mantêm enable.
Esse comportamento é exatamente o que você quer na maioria dos casos. Em outras palavras, o arquivo compartilhado guarda os padrões e cada projeto ajusta apenas as exceções.
Por outro lado, a importação antecipada traz uma limitação. Propriedades que o projeto ainda não definiu, como TargetFramework, aparecem vazias dentro do Directory.Build.props.
Controle explícito da importação
Na maioria dos casos, a busca automática basta. Mesmo assim, o MSBuild oferece propriedades para controlar esse processo quando o cenário exige mais precisão.
A propriedade ImportDirectoryBuildProps vem como true por padrão. Se você a definir como false, o MSBuild deixa de procurar o arquivo para aquele build.
Já a propriedade DirectoryBuildPropsPath indica o caminho exato do arquivo que deve ser usado. Com isso, você aponta para um arquivo fora da árvore de pastas do projeto.
O mesmo vale para o arquivo de targets, com ImportDirectoryBuildTargets e DirectoryBuildTargetsPath. Na prática, essas opções aparecem em repositórios grandes, com ferramentas de build próprias.
Outro ponto pouco conhecido é que o MSBuild lê todos os arquivos importados durante a avaliação. Ou seja, alterar o Directory.Build.props em um evento de pré-build não tem efeito no build atual.
Além disso, quando o build envolve vários projetos, o arquivo é avaliado uma vez para cada projeto. Por isso, propriedades como $(MSBuildProjectName) recebem o valor correto em cada um deles.
Exemplo prático de Directory.Build.props
Considere uma solução com uma biblioteca de classes, uma Web API e um projeto de testes. Um Directory.Build.props típico para esse cenário ficaria assim:
<Project>
<PropertyGroup>
<!-- Configurações do compilador compartilhadas -->
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- Metadados comuns a todos os assemblies -->
<Authors>Contoso Team</Authors>
<Company>Contoso</Company>
</PropertyGroup>
<ItemGroup>
<!-- Analisador aplicado a toda a solução -->
<PackageReference Include="Roslynator.Analyzers" Version="5.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Repare que o arquivo mistura dois tipos de configuração. Primeiro, temos propriedades do compilador e metadados do assembly.
Em seguida, temos uma referência a um pacote de analisadores. Com isso, todos os projetos recebem as mesmas regras de análise estática sem nenhuma linha extra.
O uso de PrivateAssets com o valor all também merece atenção. Ele impede que o analisador vire dependência de quem consome seus pacotes.
Como fica o csproj depois da mudança
Depois de adicionar o arquivo compartilhado, cada projeto fica reduzido ao que realmente é diferente. Veja como fica o .csproj da Web API:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
</ItemGroup>
</Project>
Não há mais Nullable, ImplicitUsings ou Authors repetidos. Então, o arquivo do projeto passa a comunicar apenas o que torna aquele projeto único.
Esse ganho fica ainda mais evidente em soluções grandes. Quando você precisa ativar uma regra nova, como TreatWarningsAsErrors, basta mudar um único lugar.
Se você trabalha com APIs no .NET 10, essa padronização combina bem com outras práticas de qualidade. Um exemplo é o tratamento global de exceções com IExceptionHandler, que também centraliza uma responsabilidade em um único ponto.
Arquivos aninhados não se mesclam
Este é o ponto que mais causa confusão. Quando uma subpasta tem o próprio Directory.Build.props, o MSBuild importa esse arquivo e ignora completamente o da raiz.
Ou seja, o arquivo mais próximo vence e nada é mesclado. Muita gente espera um comportamento de herança, mas ele não existe por padrão.
Pense em um cenário comum. O arquivo da raiz define Authors e TreatWarningsAsErrors, e alguém cria outro arquivo na pasta de um projeto com uma propriedade qualquer.
Nesse caso, aquele projeto perde os dois valores da raiz. Ele volta aos padrões do MSBuild, mesmo com o arquivo principal algumas pastas acima.
O pior é que nada avisa sobre essa perda. O build continua funcionando, porém sem as regras que você imaginava estarem ativas.
Como estender o Directory.Build.props da raiz
Para que um arquivo aninhado estenda o da raiz, você precisa importar o arquivo pai de forma explícita. A função GetPathOfFileAbove faz essa busca a partir da pasta acima:
<Project>
<!-- Importa o Directory.Build.props da pasta pai -->
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<!-- Propriedades específicas desta pasta -->
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>
Com essa linha de Import, os valores da raiz voltam a valer. Além disso, a pasta aninhada mantém as próprias propriedades.
A documentação da Microsoft sugere ainda adicionar uma condição ao Import. Assim, o arquivo só tenta importar o pai quando ele realmente existe.
Uma estrutura bastante usada separa as pastas src e test, cada uma com o próprio arquivo. A raiz guarda o que é comum, e cada subpasta acrescenta suas regras.
O comando dotnet new buildprops --inherit gera esse arquivo já com a linha de importação. Por isso, ele é o caminho mais seguro para criar arquivos aninhados.
Quando usar o Directory.Build.targets
O Directory.Build.props é importado cedo e, portanto, não enxerga valores que o projeto define depois. Já o Directory.Build.targets resolve esse problema, porque o MSBuild o importa no fim da avaliação.
Use o arquivo de targets quando a lógica depende de valores finais do projeto. Exemplos clássicos são AssemblyName e TargetFramework, que só ficam definidos depois do corpo do .csproj.
Veja um target que imprime informações de cada projeto após o build:
<Project>
<Target Name="PrintBuildInfo" AfterTargets="Build">
<!-- Exibe os valores finais de cada projeto -->
<Message Importance="high" Text="Building $(MSBuildProjectName) for $(TargetFramework), AssemblyName=$(AssemblyName)" />
</Target>
</Project>
Com esse arquivo na raiz, o dotnet build mostra o TargetFramework e o AssemblyName reais de cada projeto. Esses valores não estariam disponíveis dentro do Directory.Build.props.
Um erro comum é usar condições com $(TargetFramework) no arquivo de props. Em projetos com um único framework, essa condição falha em silêncio, porque a propriedade ainda está vazia.
Vale lembrar que o Directory.Build.targets segue a mesma regra de busca. Então, um arquivo aninhado também substitui o da raiz, a menos que você o importe explicitamente.
Versões de pacotes com Directory.Packages.props
Para versões de pacotes NuGet, existe um terceiro arquivo: o Directory.Packages.props. Ele ativa o recurso de gerenciamento central de pacotes, conhecido como CPM.
Você define a propriedade ManagePackageVersionsCentrally como true e lista cada versão uma única vez. Em seguida, os projetos passam a referenciar os pacotes apenas pelo nome:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Versões definidas em um só lugar -->
<PackageVersion Include="Roslynator.Analyzers" Version="5.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
</ItemGroup>
</Project>
Ao ativar o CPM, todo PackageReference que ainda tiver o atributo Version gera o erro NU1008. Isso inclui a referência ao analisador que estava no Directory.Build.props.
Portanto, remova o atributo Version dessa referência assim que a versão passar a morar no arquivo de pacotes. Os metadados PrivateAssets e IncludeAssets, no entanto, continuam no PackageReference.
Depois desse ajuste, a divisão fica clara. O Directory.Build.props cuida do compilador e dos analisadores, enquanto o Directory.Packages.props cuida das versões.
| Arquivo | Momento da importação | Uso recomendado |
|---|---|---|
Directory.Build.props |
Cedo, antes do corpo do projeto | Padrões que os projetos podem sobrescrever |
Directory.Build.targets |
Tarde, depois do corpo do projeto | Targets e propriedades que dependem de valores finais |
Directory.Packages.props |
Com o CPM habilitado | Versões de pacotes via PackageVersion |
Se você publica pacotes, há um bônus. O valor de Authors definido no arquivo compartilhado também vira o metadado de autor do pacote NuGet.
Como depurar o que foi importado
Quando uma propriedade não se comporta como esperado, adivinhar custa caro. Por isso, o MSBuild oferece algumas opções para inspecionar o resultado da avaliação.
A primeira é perguntar diretamente qual valor uma propriedade recebeu em determinado projeto:
dotnet msbuild src/Contoso.Api/Contoso.Api.csproj -getProperty:Nullable
Esse comando retorna o valor final, já considerando o Directory.Build.props e o .csproj. Como -getProperty é uma opção recente, confira em dotnet msbuild -h se o seu SDK a oferece.
A segunda opção gera uma cópia pré-processada do projeto, com todos os Import expandidos:
dotnet msbuild src/Contoso.Api/Contoso.Api.csproj -pp:out.xml
O arquivo out.xml mostra o conteúdo do Directory.Build.props ao lado dos elementos do próprio projeto. Assim, você enxerga a ordem real em que cada valor foi definido.
Logs binários com MSBuildStructuredLog
Para uma visão completa do build, grave um log binário. Basta passar a opção -bl para o dotnet build:
dotnet build -bl
Esse comando cria o arquivo msbuild.binlog ao lado da solução. O log registra targets, tasks e os valores de propriedades e itens durante o build.
Em seguida, abra o arquivo no MSBuild Structured Log Viewer, uma ferramenta gratuita criada por Kirill Osenkov. Com ela, você pesquisa o nome da propriedade e descobre qual arquivo definiu o valor.
Armadilhas comuns do Directory.Build.props
Algumas armadilhas aparecem com frequência em equipes que adotam o Directory.Build.props. A seguir, estão as quatro mais importantes.
A primeira é que o arquivo vale para todos os projetos abaixo dele, inclusive os de teste. Se os testes estão sob a mesma raiz, eles herdam tudo, a menos que tenham um arquivo próprio.
A segunda é que o aninhamento substitui em vez de mesclar. Um arquivo mais próximo faz o da raiz ser ignorado, exceto quando existe o Import explícito.
A terceira envolve a diferença entre maiúsculas e minúsculas no nome do arquivo. Windows e macOS usam sistemas de arquivos que ignoram essa diferença por padrão, enquanto o Linux não ignora.
Assim, um arquivo chamado directory.build.props funciona na sua máquina e falha no pipeline de CI em Linux. O nome precisa ser exatamente Directory.Build.props.
A quarta é confundir os momentos de importação. Use o arquivo de props para padrões sobrescrevíveis e o de targets para lógica que precisa dos valores finais.
Existe ainda um cuidado ligado ao Visual Studio. A IDE guarda informações dos projetos em cache, então recarregue a solução depois de alterar esses arquivos.
Boas práticas com Directory.Build.props
Comece pequeno e mova para o Directory.Build.props apenas o que é realmente comum. Propriedades específicas de um projeto devem continuar no próprio .csproj.
Também mantenha o arquivo versionado no Git junto com o código. Dessa forma, todos os desenvolvedores e o pipeline de CI compilam com as mesmas regras.
Evite colocar TargetFramework no arquivo compartilhado quando os projetos usam frameworks diferentes. Nesse caso, a sobrescrita constante tira o sentido da centralização.
Documente a estrutura de arquivos aninhados no README da solução. Assim, ninguém cria um arquivo novo em uma subpasta sem saber que ele substitui o da raiz.
Por fim, valide as mudanças com -getProperty antes de abrir um pull request. Essa verificação leva segundos e evita surpresas no build de produção.
Se você quer entender melhor como o ASP.NET Core organiza responsabilidades compartilhadas, vale ler sobre middleware no ASP.NET Core. A ideia de centralizar comportamento em um único ponto é a mesma.
Conclusão
O Directory.Build.props é uma das formas mais simples de padronizar uma solução .NET. Com um único arquivo, você define nullable, versão da linguagem, analisadores e metadados para todos os projetos.
Entretanto, o recurso exige atenção a três regras. O arquivo é importado cedo, o mais próximo vence e nada se mescla sem um Import explícito.
Quando a lógica depende dos valores finais do projeto, use o Directory.Build.targets. Já para versões de pacotes, o Directory.Packages.props com CPM é o caminho recomendado.
Com esses três arquivos bem divididos, seus .csproj ficam curtos e fáceis de revisar. Além disso, as regras de qualidade passam a valer para toda a equipe de forma consistente.
FAQ
Onde devo colocar o Directory.Build.props?
Normalmente na raiz do repositório, ao lado do arquivo .sln. O MSBuild procura o arquivo a partir da pasta de cada projeto e sobe até encontrar o primeiro.
Um projeto pode ignorar uma propriedade do Directory.Build.props?
Sim, basta definir a mesma propriedade no .csproj com outro valor. Como o arquivo compartilhado é importado antes, o valor do projeto prevalece.
Posso ter mais de um Directory.Build.props na solução?
Pode, mas o arquivo mais próximo substitui o da raiz. Para combinar os dois, adicione um Import com GetPathOfFileAbove no arquivo aninhado.
Qual a diferença entre Directory.Build.props e Directory.Build.targets?
O primeiro é importado antes do corpo do projeto e serve para valores padrão. O segundo é importado depois e serve para targets e propriedades que dependem dos valores finais.
Por que recebo o erro NU1008?
Esse erro aparece quando o gerenciamento central de pacotes está ativo e algum PackageReference ainda define Version. Remova o atributo e declare a versão no Directory.Packages.props.
Como desativo a importação do Directory.Build.props em um projeto?
Defina a propriedade ImportDirectoryBuildProps como false. Ela vem como true por padrão, por isso o MSBuild sempre procura o arquivo.