Documentation
adrs/051-msbuild-component-reference-properties.md
ADR-051: MSBuild Component Reference Properties
Status
Accepted
Context
The Dynaplex architecture organizes code into components, each with a standard project structure (.Abstractions, implementation, .ApiClient, .Database, tests). Projects frequently need to reference other components' projects, particularly the .Abstractions projects that define contracts.
The Problem with Relative Paths
Traditional .NET project references use relative paths:
<!-- Fragile: breaks if either project moves -->
<ProjectReference Include="../../../spatial/src/Acsis.Dynaplex.Engines.Spatial.Abstractions/Acsis.Dynaplex.Engines.Spatial.Abstractions.csproj" />
This approach has significant problems:
- Brittleness: Moving any project breaks all references to/from it
- Error-Prone: Long relative paths are easy to get wrong
- Refactoring Friction: Developers avoid restructuring because of reference update burden
- Cognitive Load: Hard to understand dependencies when paths obscure component names
- Inconsistency: Different developers might use different path styles (forward vs back slashes, different relative positions)
- IDE Confusion: Some IDEs struggle with deeply nested relative paths
Relationship to ADR-006
ADR-006 established centralized MSBuild configuration via Directory.Build.props. This ADR extends that pattern specifically for component project references.
Decision
We define MSBuild properties for all component project paths in Directory.Build.props, enabling projects to reference components by logical name rather than physical path.
Property Naming Convention
$(ComponentName) - Main implementation project
$(ComponentNameAbstractions) - Abstractions project
$(ComponentNameApiClient) - API client project
$(ComponentNameDatabase) - Database project
Implementation in Directory.Build.props
<Project>
<PropertyGroup>
<!-- Repository structure -->
<RootDir>$(MSBuildThisFileDirectory)</RootDir>
<EngineDir>$(RootDir)engines/</EngineDir>
<ProjDir>$(RootDir)projects/</ProjDir>
<StrataDir>$(RootDir)strata/</StrataDir>
<CortexDir>$(RootDir)cortex/</CortexDir>
</PropertyGroup>
<PropertyGroup>
<!-- Component reference properties -->
<!-- Catalog component -->
<CatalogAbstractions>$(EngineDir)catalog/src/Acsis.Dynaplex.Engines.Catalog.Abstractions/Acsis.Dynaplex.Engines.Catalog.Abstractions.csproj</CatalogAbstractions>
<CatalogApiClient>$(EngineDir)catalog/src/Acsis.Dynaplex.Engines.Catalog.ApiClient/Acsis.Dynaplex.Engines.Catalog.ApiClient.csproj</CatalogApiClient>
<CatalogDatabase>$(EngineDir)catalog/src/Acsis.Dynaplex.Engines.Catalog.Database/Acsis.Dynaplex.Engines.Catalog.Database.csproj</CatalogDatabase>
<!-- Identity component -->
<IdentityAbstractions>$(EngineDir)identity/src/Acsis.Dynaplex.Engines.Identity.Abstractions/Acsis.Dynaplex.Engines.Identity.Abstractions.csproj</IdentityAbstractions>
<IdentityApiClient>$(EngineDir)identity/src/Acsis.Dynaplex.Engines.Identity.ApiClient/Acsis.Dynaplex.Engines.Identity.ApiClient.csproj</IdentityApiClient>
<!-- Spatial component -->
<SpatialAbstractions>$(EngineDir)spatial/src/Acsis.Dynaplex.Engines.Spatial.Abstractions/Acsis.Dynaplex.Engines.Spatial.Abstractions.csproj</SpatialAbstractions>
<SpatialApiClient>$(EngineDir)spatial/src/Acsis.Dynaplex.Engines.Spatial.ApiClient/Acsis.Dynaplex.Engines.Spatial.ApiClient.csproj</SpatialApiClient>
<!-- Development resources -->
<DplxAnalyzers>$(DevDir)resources/analyzers/src/Acsis.Dynaplex.Strata.Analyzers/Acsis.Dynaplex.Strata.Analyzers.csproj</DplxAnalyzers>
<Dynaplex>$(DevDir)resources/dynaplex/src/Acsis.Dynaplex/Acsis.Dynaplex.csproj</Dynaplex>
<ServiceDefaults>$(DevDir)resources/dynaplex/src/Acsis.Dynaplex.Strata.ServiceDefaults/Acsis.Dynaplex.Strata.ServiceDefaults.csproj</ServiceDefaults>
</PropertyGroup>
</Project>
Usage in Project Files
<!-- Clean, intention-revealing references -->
<ItemGroup>
<ProjectReference Include="$(SpatialAbstractions)" />
<ProjectReference Include="$(CatalogApiClient)" />
<ProjectReference Include="$(Dynaplex)" />
</ItemGroup>
<!-- Analyzer reference pattern -->
<ItemGroup>
<ProjectReference Include="$(DplxAnalyzers)" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
What NOT to Do
<!-- NEVER use relative paths -->
<ProjectReference Include="../../../spatial/src/Acsis.Dynaplex.Engines.Spatial.Abstractions/Acsis.Dynaplex.Engines.Spatial.Abstractions.csproj" />
<!-- NEVER hardcode full paths -->
<ProjectReference Include="C:\source\acsis-core\components\spatial\src\Acsis.Dynaplex.Engines.Spatial.Abstractions\Acsis.Dynaplex.Engines.Spatial.Abstractions.csproj" />
<!-- NEVER mix patterns in the same project -->
<ItemGroup>
<ProjectReference Include="$(SpatialAbstractions)" /> <!-- Good -->
<ProjectReference Include="../catalog/src/..." /> <!-- Bad - inconsistent -->
</ItemGroup>
Consequences
Positive
- Refactoring Freedom: Move components without updating consumers
- Readable References:
$(SpatialAbstractions)is self-documenting - Single Update Point: Rename/move a project, update one property
- Consistency: All projects use the same reference pattern
- IDE Friendly: Properties resolve correctly in Visual Studio and Rider
- Build Reliability: No path resolution issues across platforms
- Dependency Visibility: All component relationships visible in one file
Negative
- Indirection: Extra step to find actual project path
- MSBuild Knowledge: Developers need to understand property syntax
- Maintenance: New components require adding properties
- Debugging: Build errors may reference resolved paths, not property names
Mitigation Strategies
For Indirection:
- IDE "Go to Definition" works on resolved paths
- Document property-to-path mapping in comments
For New Components:
<!-- Template for new component properties -->
<!--
<ComponentNameAbstractions>$(EngineDir)component-name/src/Acsis.Dynaplex.Engines.ComponentName.Abstractions/Acsis.Dynaplex.Engines.ComponentName.Abstractions.csproj</ComponentNameAbstractions>
<ComponentNameApiClient>$(EngineDir)component-name/src/Acsis.Dynaplex.Engines.ComponentName.ApiClient/Acsis.Dynaplex.Engines.ComponentName.ApiClient.csproj</ComponentNameApiClient>
-->
For Build Debugging:
# Show resolved property values
dotnet msbuild -getProperty:SpatialAbstractions
Adding New Component References
When creating a new component:
- Add properties to
Directory.Build.props:
<NewComponentAbstractions>$(EngineDir)new-component/src/Acsis.Dynaplex.Engines.NewComponent.Abstractions/Acsis.Dynaplex.Engines.NewComponent.Abstractions.csproj</NewComponentAbstractions>
- Reference in consuming projects:
<ProjectReference Include="$(NewComponentAbstractions)" />
Relationship to Architecture
This pattern enforces the Dynaplex architectural boundary rules:
- Components reference
.Abstractionsonly: Properties make it obvious when referencing implementation projects (which should be rare) - API Clients for cross-component calls:
$(ComponentApiClient)properties enable type-safe HTTP communication - Database isolation:
$(ComponentDatabase)properties used only within the component itself
Related Decisions
- ADR-006: Centralized MSBuild Configuration (foundation for this pattern)
- ADR-007: Microservices Architecture (why components are separate services)
- ADR-021: Kiota API Clients (why ApiClient projects exist)