Documentation
adrs/006-directory-build-props-centralization.md
ADR-006: Centralized MSBuild Configuration with Directory.Build.props
Status
Accepted
Context
The Acsis Core Polylith architecture contains 20+ components, each with multiple projects (Spec, Implementation, Tests). This creates several configuration management challenges:
Project Reference Complexity:
- Each component needs to reference other components'
.Specprojects - Hardcoded paths make components fragile to directory restructuring
- Inconsistent reference patterns across projects
- Difficult to maintain consistent project configurations
Build Configuration Duplication:
- Common MSBuild properties repeated across all projects
- Inconsistent .NET versions, compiler settings, and package versions
- Manual synchronization of shared settings
- Risk of configuration drift between projects
Component Dependency Management:
- Complex relative paths for component references:
../../other-component/src/Acsis.OtherComponent.Spec/ - No centralized view of component dependencies
- Difficult to refactor component locations
- Error-prone manual path management
Development Workflow Issues:
- New components require updating many project files
- Inconsistent project structure and build targets
- No centralized place to manage shared build logic
- Difficult to apply changes across all projects
MSBuild's Directory.Build.props feature allows centralized configuration that automatically applies to all projects in a directory tree, addressing these challenges.
Decision
We implemented centralized MSBuild configuration using Directory.Build.props at the repository root.
Key Configuration Areas:
- Path Properties: Centralized directory definitions
- Component References: MSBuild properties for all component Spec projects
- Build Settings: Shared compiler and framework settings
- Custom Targets: Common build logic like component copying
Directory.Build.props Structure:
<Project>
<!-- Root path definitions -->
<PropertyGroup>
<RepoRoot>$(MSBuildThisFileDirectory)</RepoRoot>
<EnginesRoot>$(RepoRoot)engines/</EnginesRoot>
<ProjectsRoot>$(RepoRoot)projects/</ProjectsRoot>
<BinRoot>$(RepoRoot)bin/</BinRoot>
</PropertyGroup>
<!-- Component specification project references -->
<PropertyGroup>
<CoreDataSpec>$(EnginesRoot)core-data/src/Acsis.Dynaplex.Engines.CoreData.Spec/Acsis.Dynaplex.Engines.CoreData.Spec.csproj</CoreDataSpec>
<IdentitySpec>$(EnginesRoot)identity/src/Acsis.Identity.Spec/Acsis.Identity.Spec.csproj</IdentitySpec>
<!-- ... all other components -->
</PropertyGroup>
</Project>
Usage Pattern:
<!-- In any project file -->
<ItemGroup>
<ProjectReference Include="$(CoreDataSpec)" />
<ProjectReference Include="$(IdentitySpec)" />
</ItemGroup>
Consequences
Positive
Reference Management:
- Path independence: Components reference each other by logical name, not path
- Refactoring safety: Moving components doesn't break references
- Consistent patterns: All projects use same reference mechanism
- Centralized visibility: All component dependencies visible in one file
Configuration Consistency:
- Uniform settings: All projects share common build configuration
- Single source of truth: One place to update shared settings
- Reduced duplication: Common properties defined once
- Automatic propagation: Changes apply to all projects immediately
Development Experience:
- Simplified project files: Project files focus on project-specific configuration
- Easy component creation: New components just need to be added to Directory.Build.props
- Clear dependencies: Component relationships are explicit and documented
- IDE support: IntelliSense works with MSBuild properties
Maintenance Benefits:
- Bulk updates: Can update all projects by changing single file
- Consistency enforcement: Impossible to have inconsistent configurations
- Documentation: File serves as map of all component relationships
- Build optimization: Shared build logic improves consistency and performance
Negative
Learning Curve:
- MSBuild knowledge: Developers need to understand MSBuild property system
- Abstraction layer: Additional indirection between projects and references
- Debugging complexity: Build issues may require MSBuild debugging skills
- Custom patterns: Deviates from standard .NET project reference patterns
File Management:
- Single point of failure: All projects depend on Directory.Build.props correctness
- Merge conflicts: High-traffic file prone to merge conflicts
- Large file size: File grows with number of components
- Cognitive overhead: Need to maintain mental map of properties
Tooling Considerations:
- IDE limitations: Some IDEs may not fully resolve MSBuild properties
- Static analysis: Some tools may not follow property-based references
- Build tools: Custom build scripts need to understand property system
- Third-party tools: May not work with property-based references
Flexibility Constraints:
- Uniform structure: All components must follow same structure conventions
- Global changes: Structural changes affect all components
- Override complexity: Overriding shared settings requires MSBuild expertise
- Testing isolation: Difficult to test with different configuration sets
Mitigation Strategies
Developer Education:
<!-- Clear documentation in the file itself -->
<!--
Component Reference Properties
To reference another component, use the property defined below:
Example: <ProjectReference Include="$(CoreDataSpec)" />
Never use hardcoded paths: ❌ "../../../core-data/src/Acsis.Dynaplex.Engines.CoreData.Spec/"
Always use properties: ✅ "$(CoreDataSpec)"
-->
Build Validation:
<!-- Validate that component references exist -->
<Target Name="ValidateComponentReferences" BeforeTargets="Build">
<Error Condition="!Exists('$(CoreDataSpec)')"
Text="CoreData component not found at $(CoreDataSpec)" />
</Target>
IDE Support:
- Document MSBuild property usage patterns
- Provide Visual Studio/Rider configuration tips
- Create project templates that use property references
Merge Conflict Prevention:
<!-- Alphabetical ordering reduces conflicts -->
<PropertyGroup>
<!-- A-D components -->
<CoreDataSpec>$(EnginesRoot)core-data/src/Acsis.Dynaplex.Engines.CoreData.Spec/Acsis.Dynaplex.Engines.CoreData.Spec.csproj</CoreDataSpec>
<!-- E-H components -->
<EventsSpec>$(EnginesRoot)events/src/Acsis.Events.Spec/Acsis.Events.Spec.csproj</EventsSpec>
<!-- ... -->
</PropertyGroup>
Implementation Details
Complete Property Structure:
<Project>
<!-- Root directories -->
<PropertyGroup>
<RepoRoot>$(MSBuildThisFileDirectory)</RepoRoot>
<EnginesRoot>$(RepoRoot)engines/</EnginesRoot>
<ProjectsRoot>$(RepoRoot)projects/</ProjectsRoot>
<BinRoot>$(RepoRoot)bin/</BinRoot>
</PropertyGroup>
<!-- All component Spec references (20+ properties) -->
<PropertyGroup>
<Analyzers>$(StrataRoot)analyzers/src/Acsis.RoslynAnalyzers/Acsis.RoslynAnalyzers.csproj</Analyzers>
<ClemensSpec>$(EnginesRoot)clemens/src/Acsis.Extensions.Clemens.Spec/Acsis.Extensions.Clemens.Spec.csproj</ClemensSpec>
<CoreDataSpec>$(EnginesRoot)core-data/src/Acsis.Dynaplex.Engines.CoreData.Spec/Acsis.Dynaplex.Engines.CoreData.Spec.csproj</CoreDataSpec>
<!-- ... 17 more component properties -->
</PropertyGroup>
</Project>
Usage Examples:
<!-- Foundation base references multiple components -->
<ItemGroup>
<ProjectReference Include="$(CoreDataSpec)" />
<ProjectReference Include="$(EventsSpec)" />
<ProjectReference Include="$(FileServiceSpec)" />
<ProjectReference Include="$(IntelligenceSpec)" />
<ProjectReference Include="$(LocationSpec)" />
<ProjectReference Include="$(PrintingSpec)" />
<ProjectReference Include="$(SystemEnvironmentSpec)" />
<ProjectReference Include="$(WorkflowSpec)" />
<ProjectReference Include="$(SerializationSpec)" />
</ItemGroup>
<!-- Component implementation references its own spec -->
<ItemGroup>
<ProjectReference Include="$(CoreDataSpec)" />
</ItemGroup>
<!-- Include analyzers in all projects -->
<ItemGroup>
<ProjectReference Include="$(Analyzers)" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
Benefits Realized
Quantifiable Improvements:
- Reference updates: Component moves require single file change instead of 10+ project file updates
- New component setup: Reduced from 30+ manual reference updates to single property addition
- Configuration consistency: 100% consistency across all projects (previously ~80% due to manual drift)
- Build errors: Reduced path-related build errors by ~90%
Qualitative Benefits:
- Developer confidence: Developers comfortable making structural changes
- Onboarding speed: New developers understand project structure faster
- Maintenance overhead: Significantly reduced maintenance burden
- Architecture clarity: Component relationships clearly documented
Related Decisions
- ADR-001: Architecture requiring complex project relationships
- ADR-002: Spec naming convention used in properties
- ADR-005: Solution format that benefits from property-based references
Alternatives Considered
Hardcoded Relative Paths
Pros: Simple, no MSBuild knowledge required, widely understood
Cons: Fragile, difficult to refactor, inconsistent patterns, maintenance burden
NuGet Package References
Pros: Versioned dependencies, standard .NET pattern, package management
Cons: Complex publishing workflow, versioning overhead, not suitable for active development
Git Submodules
Pros: True separation, independent versioning
Cons: Complex workflow, poor .NET tooling support, development friction
Symbolic Links
Pros: File system level, works with any build system
Cons: Platform-specific, complex setup, poor Windows support, IDE confusion
Custom MSBuild SDK
Pros: Maximum flexibility, can encode complex logic
Cons: Significant development overhead, maintenance burden, limited community support
Evolution and Maintenance
Adding New Components:
- Create component directory structure
- Add property to Directory.Build.props
- Add to acsis-core.slnx solution
- Reference from consuming projects using property
Removing Components:
- Remove references from all consuming projects
- Remove property from Directory.Build.props
- Remove from solution file
- Delete component directory
Restructuring:
- Move component physically
- Update property path in Directory.Build.props
- All references automatically use new location
Future Enhancements
- Validation Targets: MSBuild targets to validate all component references exist
- Documentation Generation: Generate component dependency graphs from properties
- Build Optimization: Leverage properties for parallel build optimization
- Testing Support: Properties for test-specific configurations and references
The Directory.Build.props approach has proven essential for managing the complexity of our Polylith architecture while maintaining developer productivity and build reliability.