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' .Spec projects
  • 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:

  1. Path Properties: Centralized directory definitions
  2. Component References: MSBuild properties for all component Spec projects
  3. Build Settings: Shared compiler and framework settings
  4. 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
  • 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

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:

  1. Create component directory structure
  2. Add property to Directory.Build.props
  3. Add to acsis-core.slnx solution
  4. Reference from consuming projects using property

Removing Components:

  1. Remove references from all consuming projects
  2. Remove property from Directory.Build.props
  3. Remove from solution file
  4. Delete component directory

Restructuring:

  1. Move component physically
  2. Update property path in Directory.Build.props
  3. All references automatically use new location

Future Enhancements

  1. Validation Targets: MSBuild targets to validate all component references exist
  2. Documentation Generation: Generate component dependency graphs from properties
  3. Build Optimization: Leverage properties for parallel build optimization
  4. 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.