Quando trabalhamos com soluções .NET que possuem múltiplos projetos, a manutenção pode se tornar um pesadelo. Imagine ter que atualizar a versão do Newtonsoft.Json em 15 projetos diferentes, ou garantir que todos os projetos usem as mesmas configurações de compilação. É exatamente para resolver esses problemas que existem recursos como Central Package Management, Directory.Build.props/targets e .editorconfig.
Neste artigo, vamos explorar cada um desses recursos e entender como eles podem transformar a organização do seu projeto.
O Problema: Caos em soluções Multi-Projeto
Antes de mergulharmos nas soluções, vamos entender o problema. Em uma solução típica com vários projetos, você provavelmente já enfrentou situações como:
Versões diferentes do mesmo pacote NuGet em projetos distintos, causando conflitos em runtime. Configurações de compilação repetidas em cada arquivo .csproj, tornando mudanças globais trabalhosas. Estilos de código inconsistentes entre desenvolvedores da equipe, gerando commits desnecessários apenas para formatação.
Esses problemas não apenas consomem tempo, mas também introduzem bugs sutis e dificultam a manutenção do código a longo prazo.
Central Package Management: Uma única fonte de verdade para versões
O Central Package Management (CPM) foi introduzido no NuGet 6.2 e permite centralizar todas as versões de pacotes em um único arquivo. Em vez de especificar versões em cada projeto, você define todas elas em um arquivo Directory.Packages.props na raiz da solução.
Habilitando o Central Package Management
Primeiro, crie um arquivo Directory.Packages.props na raiz da sua solução:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="FluentValidation" Version="11.9.0" />
<PackageVersion Include="Polly" Version="8.2.0" />
</ItemGroup>
</Project>
Referenciando Pacotes nos Projetos
Com o CPM habilitado, seus arquivos .csproj ficam mais limpos. Note que você não especifica mais a versão:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Serilog" />
</ItemGroup>
</Project>
Sobrescrevendo versões quando necessário
Em casos excepcionais onde um projeto específico precisa de uma versão diferente, você pode usar o atributo VersionOverride:
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" VersionOverride="12.0.3" />
</ItemGroup>
Isso é útil durante migrações graduais ou quando existe uma incompatibilidade específica em um projeto legado.
Organizando pacotes com rótulos
Para soluções grandes, você pode organizar os pacotes usando comentários ou até mesmo arquivos separados que são importados:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- Frameworks e Runtime -->
<ItemGroup Label="Microsoft">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<!-- Logging e Observabilidade -->
<ItemGroup Label="Observability">
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageVersion Include="OpenTelemetry" Version="1.7.0" />
</ItemGroup>
<!-- Testes -->
<ItemGroup Label="Testing">
<PackageVersion Include="xunit" Version="2.6.6" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
</Project>
Directory.Build.props: Configurações compartilhadas
O arquivo Directory.Build.props é automaticamente importado por todos os projetos no diretório onde ele está localizado (e em subdiretórios). Ele é processado no início do build, antes das configurações do projeto.
Criando o Directory.Build.props
Coloque este arquivo na raiz da solução:
<Project>
<!-- Configurações gerais de compilação -->
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
<!-- Informações do assembly -->
<PropertyGroup>
<Company>MinhaEmpresa</Company>
<Authors>Time de Desenvolvimento</Authors>
<Copyright>Copyright © MinhaEmpresa 2026</Copyright>
<RepositoryUrl>https://github.com/minhaempresa/meu-projeto</RepositoryUrl>
</PropertyGroup>
<!-- Configurações de output -->
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>
Configurações Condicionais
Você pode aplicar configurações diferentes baseadas em condições:
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<!-- Configurações apenas para projetos de teste -->
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))">
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<!-- Configurações apenas para builds de Release -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<Optimize>true</Optimize>
</PropertyGroup>
<!-- Analisadores apenas em Debug -->
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Hierarquia de Directory.Build.props
O MSBuild procura por arquivos Directory.Build.props subindo na árvore de diretórios, mas por padrão para no primeiro que encontrar. Se você precisa de configurações em múltiplos níveis, use a importação explícita:
/solution
├── Directory.Build.props (configurações globais)
├── src/
│ ├── Directory.Build.props (configurações específicas de src)
│ ├── Api/
│ │ └── Api.csproj
│ └── Domain/
│ └── Domain.csproj
└── tests/
├── Directory.Build.props (configurações específicas de testes)
└── Api.Tests/
└── Api.Tests.csproj
No src/Directory.Build.props, importe o arquivo pai:
<Project>
<!-- Importa as configurações globais primeiro -->
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<!-- Configurações específicas para projetos em src/ -->
<PropertyGroup>
<IsPackable>true</IsPackable>
</PropertyGroup>
</Project>
Directory.Build.targets: Ações Pós-Processamento
Enquanto o Directory.Build.props é importado no início do build, o Directory.Build.targets é importado no final, depois que todas as propriedades e itens do projeto foram definidos. Isso o torna ideal para tarefas que dependem de valores computados ou para adicionar targets customizados.
Casos de Uso Típicos
<Project>
<!-- Target que roda após o build -->
<Target Name="PrintBuildInfo" AfterTargets="Build">
<Message Importance="High" Text="Build concluído: $(MSBuildProjectName) - $(Configuration)" />
</Target>
<!-- Copiar arquivos de configuração -->
<Target Name="CopyConfigFiles" AfterTargets="Build"
Condition="Exists('$(ProjectDir)appsettings.local.json')">
<Copy SourceFiles="$(ProjectDir)appsettings.local.json"
DestinationFolder="$(OutputPath)"
SkipUnchangedFiles="true" />
</Target>
<!-- Gerar arquivo de versão -->
<Target Name="GenerateVersionFile" BeforeTargets="Build">
<PropertyGroup>
<BuildTimestamp>$([System.DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))</BuildTimestamp>
</PropertyGroup>
<WriteLinesToFile File="$(OutputPath)version.txt"
Lines="Version: $(Version);Built: $(BuildTimestamp)"
Overwrite="true" />
</Target>
<!-- Validação de estrutura do projeto -->
<Target Name="ValidateProjectStructure" BeforeTargets="Build">
<Error Condition="!Exists('$(ProjectDir)README.md')"
Text="Cada projeto deve ter um README.md" />
</Target>
</Project>
Diferença entre Props e Targets
A distinção é importante para evitar comportamentos inesperados:
O Directory.Build.props é processado antes do arquivo .csproj. Use-o para definir valores padrão que o projeto pode sobrescrever.
O Directory.Build.targets é processado depois do arquivo .csproj. Use-o para ações que dependem de valores finais ou para adicionar targets que referenciam propriedades do projeto.
Por exemplo, se você quer definir uma propriedade baseada no nome do projeto:
<!-- Directory.Build.targets (CORRETO) -->
<Project>
<PropertyGroup>
<PackageId Condition="'$(PackageId)' == ''">MinhaEmpresa.$(MSBuildProjectName)</PackageId>
</PropertyGroup>
</Project>
Se isso estivesse em Directory.Build.props, o $(MSBuildProjectName) poderia não estar disponível ainda.
EditorConfig: Consistência de estilo de código
O arquivo .editorconfig define regras de formatação e estilo de código que são respeitadas pela maioria das IDEs modernas, incluindo Visual Studio, VS Code e Rider. Isso garante que todos os desenvolvedores do time sigam as mesmas convenções.
Estrutura Básica do EditorConfig
Crie um arquivo .editorconfig na raiz da solução:
# Arquivo raiz do EditorConfig
root = true
# Configurações para todos os arquivos
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# Configurações específicas para C#
[*.cs]
# Organização de usings
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
# Preferências de this.
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# Preferências de tipos
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
# Preferências de modificadores
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
# Preferências de expressão
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
# Preferências de null checking
dotnet_style_coalesce_expression = true:warning
dotnet_style_null_propagation = true:warning
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
# Configurações de formatação C#
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
# Indentação
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_block_contents = true
csharp_indent_braces = false
# Espaçamento
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_method_call_parameter_list_parentheses = false
# Wrapping
csharp_preserve_single_line_statements = false
csharp_preserve_single_line_blocks = true
# Estilos de código
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning
csharp_style_prefer_switch_expression = true:suggestion
csharp_style_prefer_pattern_matching = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
# Null checking
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:warning
# Preferências de código
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_prefer_primary_constructors = true:suggestion
# Using directives
csharp_using_directive_placement = outside_namespace:warning
# Configurações de XML/XAML
[*.{xml,csproj,props,targets}]
indent_size = 2
# Configurações de JSON
[*.json]
indent_size = 2
# Configurações de YAML
[*.{yml,yaml}]
indent_size = 2
# Markdown
[*.md]
trim_trailing_whitespace = false
Níveis de severidade
O EditorConfig permite definir diferentes níveis de severidade para cada regra:
none desabilita a regra completamente. silent ou refactoring aplica a regra apenas para refatorações, sem mostrar no editor. suggestion mostra como sugestão (pontos verdes no Visual Studio). warning mostra como aviso durante o build. error mostra como erro e falha o build.
Convenções de Nomenclatura
O EditorConfig também suporta regras de nomenclatura, essenciais para manter consistência:
[*.cs]
# Definição de símbolos
dotnet_naming_symbols.public_symbols.applicable_kinds = property,method,field,event,delegate
dotnet_naming_symbols.public_symbols.applicable_accessibilities = public,internal,protected,protected_internal
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_symbols.const_fields.applicable_kinds = field
dotnet_naming_symbols.const_fields.required_modifiers = const
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.async_methods.applicable_kinds = method
dotnet_naming_symbols.async_methods.required_modifiers = async
# Estilos de nomenclatura
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.camel_case_with_underscore.capitalization = camel_case
dotnet_naming_style.camel_case_with_underscore.required_prefix = _
dotnet_naming_style.interface_style.capitalization = pascal_case
dotnet_naming_style.interface_style.required_prefix = I
dotnet_naming_style.async_suffix.capitalization = pascal_case
dotnet_naming_style.async_suffix.required_suffix = Async
# Regras
dotnet_naming_rule.public_members_pascal_case.symbols = public_symbols
dotnet_naming_rule.public_members_pascal_case.style = pascal_case
dotnet_naming_rule.public_members_pascal_case.severity = warning
dotnet_naming_rule.private_fields_camel_case.symbols = private_fields
dotnet_naming_rule.private_fields_camel_case.style = camel_case_with_underscore
dotnet_naming_rule.private_fields_camel_case.severity = warning
dotnet_naming_rule.const_fields_pascal_case.symbols = const_fields
dotnet_naming_rule.const_fields_pascal_case.style = pascal_case
dotnet_naming_rule.const_fields_pascal_case.severity = warning
dotnet_naming_rule.interfaces_begin_with_i.symbols = interfaces
dotnet_naming_rule.interfaces_begin_with_i.style = interface_style
dotnet_naming_rule.interfaces_begin_with_i.severity = error
dotnet_naming_rule.async_methods_end_with_async.symbols = async_methods
dotnet_naming_rule.async_methods_end_with_async.style = async_suffix
dotnet_naming_rule.async_methods_end_with_async.severity = suggestion
Estrutura final recomendada
Combinando tudo, essa seria uma estrutura de diretórios recomendada:
/MinhaSolucao
├── .editorconfig
├── Directory.Build.props
├── Directory.Build.targets
├── Directory.Packages.props
├── MinhaSolucao.sln
├── src/
│ ├── Directory.Build.props (opcional, para configs específicas)
│ ├── MinhaSolucao.Api/
│ │ └── MinhaSolucao.Api.csproj
│ ├── MinhaSolucao.Domain/
│ │ └── MinhaSolucao.Domain.csproj
│ └── MinhaSolucao.Infrastructure/
│ └── MinhaSolucao.Infrastructure.csproj
└── tests/
├── Directory.Build.props (opcional, para configs de teste)
├── MinhaSolucao.Api.Tests/
│ └── MinhaSolucao.Api.Tests.csproj
└── MinhaSolucao.Domain.Tests/
└── MinhaSolucao.Domain.Tests.csproj
Dicas práticas
Comece pelo Central Package Management se você tem problemas frequentes com versões conflitantes de pacotes. É a mudança com maior impacto imediato.
O TreatWarningsAsErrors no Directory.Build.props força a equipe a manter o código limpo. Combine com NoWarn para ignorar conscientemente warnings específicos.
Use o EditorConfig com a severidade warning para regras importantes, mas não críticas, e error apenas para convenções que realmente não podem ser violadas (como o prefixo I em interfaces).
Versione esses arquivos no Git. Eles fazem parte da infraestrutura do projeto tanto quanto o código.
Ao migrar um projeto existente, faça incrementalmente. Comece com configurações menos restritivas e aumente a severidade conforme a equipe se adapta.
Conclusão
Investir tempo na configuração adequada da estrutura do projeto traz retornos significativos ao longo do ciclo de vida do software. Central Package Management elimina conflitos de versão e simplifica atualizações. Directory.Build.props/targets centraliza configurações e automatiza tarefas repetitivas. EditorConfig garante a consistência do código, independentemente da IDE ou do desenvolvedor.
Juntos, esses recursos transformam uma coleção de projetos em uma solução coesa. O esforço inicial de configuração se paga rapidamente em produtividade, menos bugs e código mais fácil de manter.
Gostou do artigo? Tem alguma configuração adicional que você usa em seus projetos? Compartilhe nos comentários!
Top comments (0)