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:

  1. Brittleness: Moving any project breaks all references to/from it
  2. Error-Prone: Long relative paths are easy to get wrong
  3. Refactoring Friction: Developers avoid restructuring because of reference update burden
  4. Cognitive Load: Hard to understand dependencies when paths obscure component names
  5. Inconsistency: Different developers might use different path styles (forward vs back slashes, different relative positions)
  6. 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

  1. Refactoring Freedom: Move components without updating consumers
  2. Readable References: $(SpatialAbstractions) is self-documenting
  3. Single Update Point: Rename/move a project, update one property
  4. Consistency: All projects use the same reference pattern
  5. IDE Friendly: Properties resolve correctly in Visual Studio and Rider
  6. Build Reliability: No path resolution issues across platforms
  7. Dependency Visibility: All component relationships visible in one file

Negative

  1. Indirection: Extra step to find actual project path
  2. MSBuild Knowledge: Developers need to understand property syntax
  3. Maintenance: New components require adding properties
  4. 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:

  1. Add properties to Directory.Build.props:
<NewComponentAbstractions>$(EngineDir)new-component/src/Acsis.Dynaplex.Engines.NewComponent.Abstractions/Acsis.Dynaplex.Engines.NewComponent.Abstractions.csproj</NewComponentAbstractions>
  1. Reference in consuming projects:
<ProjectReference Include="$(NewComponentAbstractions)" />

Relationship to Architecture

This pattern enforces the Dynaplex architectural boundary rules:

  • Components reference .Abstractions only: 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
  • 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)

References