Documentation
adrs/052-semantic-versioning-strategy.md
ADR-052: Semantic Versioning Strategy with Nerdbank.GitVersioning
Status
Proposed
Context
As the Dynaplex platform matures toward production readiness, we need a formal versioning strategy that:
- Provides platform-level versioning: A unified version that represents the overall Acsis Dynaplex release (e.g.,
9.0.0-alpha) - Supports component-level versioning: Each component should track its own changes independently, so API consumers see version increments only when that specific API changes
- Auto-increments build numbers: Avoid manual version bumping for every commit
- Integrates with CI/CD: Works seamlessly with GitLab pipelines
- Exposes version to consumers: API responses should include version information for debugging and compatibility checking
Tool Evaluation
| Tool | Approach | Pros | Cons |
|---|---|---|---|
| Nerdbank.GitVersioning | version.json + git height |
Monorepo support, path filters, ThisAssembly generation |
Medium complexity |
| MinVer | Git tags only | Simple, no config files | No path filtering, tag-dependent |
| GitVersion | Branch names + YAML config | Feature-rich | Complex, opinionated branching |
Nerdbank.GitVersioning is the best fit because:
- Supports hierarchical
version.jsonwith inheritance for monorepos - Path filters enable per-component version tracking
- Generates
ThisAssemblyclass for runtime version access - Well-maintained (.NET Foundation project)
Decision
We will use Nerdbank.GitVersioning with a dual-level versioning strategy:
- Platform Version: Root
version.jsondefines the overall Acsis Dynaplex version - Component Versions: Each component inherits the platform version but uses path filters so its git height only increments when that component changes
- Version Header: All API responses include an
X-Acsis-Versionheader showing the component's version
Version Format
{major}.{minor}.{patch}-{prerelease}.{height}+{commit}
Examples:
9.0.0-alpha.47+a1b2c3d4- Alpha build, 47th commit to this component9.0.0-beta.12+e5f6g7h8- Beta build9.0.0+i9j0k1l2- Stable release
Starting Configuration
| Setting | Value |
|---|---|
| Platform Version | 9.0-alpha |
| Pre-release Label | alpha (graduating to beta, then stable) |
| Release Strategy | Tag releases from main branch |
| Version Exposure | X-Acsis-Version HTTP response header |
Consequences
Positive
- Accurate API versioning: Component versions only change when that component changes
- Automatic build numbers: Git height eliminates manual version management
- Platform coherence: All components share the same base version (9.0)
- Runtime access:
ThisAssemblyclass provides version info for logging, health checks - CI/CD integration: Works with GitLab pipelines out of the box
- Debugging: Full version with commit hash aids troubleshooting
Negative
- Multiple version.json files: Each component needs its own file (though they're simple)
- Git depth requirement: CI/CD must fetch full history (
GIT_DEPTH: 0) - Build dependency: Adds NuGet package to all projects
- Learning curve: Team must understand path filters and inheritance
Neutral
- Height resets: When base version changes (e.g., alpha → beta), heights reset to 1
- Commit-based: Version changes on every commit (to that component), not on release
Implementation Notes
Package Setup
Directory.Packages.props (central package management):
<!-- Semantic versioning from git -->
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
Directory.Build.props (applies to all projects):
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
Root version.json (Platform Version)
File: version.json (repository root)
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "9.0-alpha",
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/tags/v\\d+\\.\\d+"
],
"cloudBuild": {
"buildNumber": {
"enabled": true
}
},
"release": {
"tagName": "v{version}",
"versionIncrement": "minor",
"firstUnstableTag": "alpha"
}
}
Component version.json (Per-Component)
Template (same for each component):
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"inherit": true,
"pathFilters": ["."]
}
Files to create:
engines/catalog/version.jsonengines/identity/version.jsonengines/prism/version.jsonengines/spatial/version.jsonengines/workflow/version.jsonengines/transport/version.jsonengines/iot/version.jsonengines/events/version.jsonengines/printing/version.jsonengines/intelligence/version.jsonengines/file-handling/version.jsonengines/core-data/version.jsonengines/system-environment/version.jsonengines/bbu/version.json
How Path-Filtered Versioning Works
Each component's version.json with "pathFilters": ["."] means:
- Inherits the base version
9.0-alphafrom the root - Git height only counts commits that touch files in that component's directory
- Different components will have different heights based on their change frequency
Initial state (after setup):
- Catalog: 9.0.0-alpha.1
- Identity: 9.0.0-alpha.1
- Spatial: 9.0.0-alpha.1
After 5 commits touching only Catalog:
- Catalog: 9.0.0-alpha.6
- Identity: 9.0.0-alpha.1 (unchanged)
- Spatial: 9.0.0-alpha.1 (unchanged)
After 3 commits touching Identity:
- Catalog: 9.0.0-alpha.6
- Identity: 9.0.0-alpha.4
- Spatial: 9.0.0-alpha.1 (still unchanged)
GitLab CI/CD Configuration
File: .gitlab-ci.yml
variables:
SECRET_DETECTION_ENABLED: 'true'
GIT_DEPTH: 0 # Required for Nerdbank.GitVersioning git height calculation
Version Header Middleware
File: strata/service-defaults/src/Acsis.Dynaplex.Strata.ServiceDefaults/Extensions.cs
/// <summary>
/// Adds the X-Acsis-Version header to all HTTP responses.
/// Uses ThisAssembly.AssemblyInformationalVersion which reflects
/// the component's path-filtered version.
/// </summary>
public static IApplicationBuilder UseAcsisVersionHeader(this IApplicationBuilder app)
{
return app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers["X-Acsis-Version"] = ThisAssembly.AssemblyInformationalVersion;
return Task.CompletedTask;
});
await next();
});
}
Register in MapAcsisEndpoints():
app.UseCors();
app.UseAcsisVersionHeader(); // Add version header to all responses
app.UseExceptionHandler();
Version in HTTP Responses
When calling /api/catalog/items:
HTTP/1.1 200 OK
X-Acsis-Version: 9.0.0-alpha.6+abc1234
Content-Type: application/json
When calling /api/identity/users:
HTTP/1.1 200 OK
X-Acsis-Version: 9.0.0-alpha.4+def5678
Content-Type: application/json
Runtime Version Access
Nerdbank.GitVersioning generates a ThisAssembly class:
// Available in any component
public class HealthService
{
public HealthInfo GetHealth()
{
return new HealthInfo
{
Version = ThisAssembly.AssemblyInformationalVersion,
// "9.0.0-alpha.47+a1b2c3d4"
SemanticVersion = ThisAssembly.AssemblyFileVersion,
// "9.0.0.47"
GitCommitId = ThisAssembly.GitCommitId,
// "a1b2c3d4e5f6..."
};
}
}
CLI Tool (Optional)
Install for local version management:
dotnet tool install -g nbgv
Commands:
# Show current version
nbgv get-version
# Show specific component's version
nbgv get-version --project engines/catalog
# Prepare a release (bumps version, optionally creates tag)
nbgv prepare-release
Version Lifecycle
Development Phase (Current)
// version.json
"version": "9.0-alpha"
Results in: 9.0.0-alpha.{height}
Beta Phase
// version.json
"version": "9.0-beta"
Results in: 9.0.0-beta.{height} (heights reset)
Stable Release
// version.json
"version": "9.0"
Results in: 9.0.0 for tagged commits, 9.0.0+{commit} for non-release builds
Next Development Cycle
// version.json
"version": "9.1-alpha"
Results in: 9.1.0-alpha.{height}
Best Practices
- Bump version deliberately: Only update root
version.jsonwhen graduating phases (alpha → beta → stable) - Don't edit component version.json: They should remain as simple inheritance files
- Tag releases: Use
git tag v9.0.0for official releases - Full git history in CI: Always use
GIT_DEPTH: 0in pipelines - Use version in logs: Include
ThisAssembly.AssemblyInformationalVersionin startup logs - Document breaking changes: When incrementing major version, document in release notes
Related ADRs
- ADR-007: Evolution to .NET Aspire Microservices (service architecture)
- ADR-011: Flexible API Versioning Strategy (API path versioning)
- ADR-019: OpenTelemetry Integration (version in telemetry)
- ADR-021: Microsoft Kiota for API Client Generation (client versioning)