Documentation

adrs/046-library-projects.md

ADR-046: Library Projects in Dynaplex

Status

Accepted

Updated: 2026-01-08 (db-manager consolidation)

Scope

This ADR defines runtime library projects only—pure, stateless code packages that are loaded into microservices and used at runtime.

Excluded from this ADR: Development tooling such as Dynaplex infrastructure, Roslyn analyzers, build scripts, project templates, and compatibility shims. These are governed by separate conventions and live in strata/ under the Acsis.Dynaplex.* namespace.

Context

The Dynaplex architecture currently distinguishes between three primary concepts:

  • Components: Independent microservices following the 4-project pattern (.Abstractions, .Database, service, .ApiClient)
  • Projects: Deployment compositions that combine components into deployable solutions
  • Bases: Orchestration helpers that assist in composing components for deployment scenarios

However, some runtime code doesn't fit cleanly into the component model. This code is:

  • Pure, stateless utilities (encoding, parsing, validation)
  • Shared domain primitives (strongly-typed IDs, value objects)
  • Runtime infrastructure abstractions (observability, resilience)

Currently, these are handled via an ad-hoc whitelist in AbstractionReferenceRequirementAnalyzer:

private static readonly string[] _allowedProjects = [
    "Acsis.Dynaplex",
    "Acsis.Dynaplex.RoslynAnalyzers",
    "Acsis.Dynaplex.EntLibCompatShim",
    "Acsis.Dynaplex.Strata.ServiceDefaults",
    "Acsis.Dynaplex.Strata.Orchestration",
    "Acsis.Encoding.Gs1"
];

This approach has several problems:

  1. No governance: Any project can be added to the whitelist without criteria
  2. Unclear intent: New developers don't understand why these projects are "special"
  3. Hidden architecture: The concept exists but isn't documented or formalized
  4. Conflated concerns: Development tooling and runtime libraries are mixed together
  5. No enforcement: Nothing prevents these "library" projects from accumulating inappropriate dependencies

The team has identified a need to share certain code directly (without HTTP overhead) while maintaining the strict boundaries that make Dynaplex valuable. The concern is that introducing a formal "library" concept could become an escape hatch that developers abuse to bypass component isolation.

Decision

We will introduce a formal Library project type as a first-class architectural concept in Dynaplex.

Definition

A Library is a pure, stateless code package that:

  • Provides cross-cutting utilities or shared domain primitives
  • Can be directly referenced by components, bases, and projects
  • Does NOT run as a service, own persistence, or expose endpoints
  • Is subject to strict dependency constraints enforced by Roslyn analyzers

Folder Structure and Naming

Runtime libraries live in a dedicated libraries/ root directory:

libraries/
├── encoding/
│   └── src/
│       └── Acsis.Dynaplex.Strata.Gs1/
│
├── domain-primitives/
│   └── src/
│       └── Acsis.Dynaplex.Strata.DomainPrimitives/
│
├── validation/
│   └── src/
│       └── Acsis.Dynaplex.Strata.Validation/
│
└── infrastructure/
    └── src/
        ├── Acsis.Dynaplex.Strata.Observability/
        └── Acsis.Dynaplex.Strata.Resilience/

Naming convention: Acsis.Dynaplex.Strata.{Name}

The Acsis.Dynaplex.Strata.* namespace prefix serves as a strong signal that a project:

  • Is safe to reference as a runtime dependency
  • Is pure, stateless, and side-effect-free
  • Meets all library qualification criteria

Note: Development tooling (Dynaplex, analyzers, compatibility shims) uses the Acsis.Dynaplex.* namespace and lives in strata/.

Qualification Criteria

A project qualifies as a library when ALL of these are true:

Criterion Description
No persistence Does not own a database schema, has no DbContext, no migrations
No endpoints Has no Program.cs, no HTTP endpoints, no background services
Pure logic Deterministic, side-effect-free (or near-pure with only logging)
Cross-cutting Used by ≥2 components, or by components AND bases/projects
Stable semantics Changes rarely; when it does, changes are coordinated globally
No business workflows No domain policies, no orchestration, no service coordination

The "workflow test": If you can describe the code as "business rule for [bounded context]" or "process that coordinates [services]", it belongs in a component, not a library.

What Libraries ARE (Examples)

Category Examples
Technical utilities GS1/barcode encoding, parsing helpers, serialization utilities
Domain primitives Strongly-typed IDs (PTIDs, PassportId), value objects, units of measure
Validation Attribute-based validators, FluentValidation extensions, common rules
Runtime infrastructure Observability helpers, resilience patterns, messaging abstractions

What Libraries ARE NOT

Anti-pattern Why it's wrong Where it belongs
Business workflows Libraries can't orchestrate processes Component service
Domain logic for one context Not cross-cutting Component's .Abstractions
Code that calls other services Libraries must be pure Component service
Code that owns data Libraries can't have persistence Component's .Database
HTTP endpoints Libraries can't be deployed Component service

Dependency Rules (Enforced by Analyzers)

Libraries CANNOT reference:

Forbidden Rationale
Acsis.Dynaplex.Engines.* Libraries must be independent of components
*.Database projects Libraries cannot depend on persistence
*.ApiClient projects Libraries cannot call other services
Microsoft.EntityFrameworkCore.* Libraries cannot own data
Microsoft.AspNetCore.* Libraries cannot expose endpoints
Direct HttpClient usage Libraries should not make HTTP calls

Libraries CAN reference:

Allowed Examples
Other Acsis.Dynaplex.Strata.* Library can use shared primitives from another library
BCL/System namespaces System.Text.Json, System.Collections.Generic, etc.
Logging abstractions Microsoft.Extensions.Logging.Abstractions
Configuration abstractions Microsoft.Extensions.Options
Pure utility packages Humanizer, Polly (for retry logic types), etc.

Reference Direction Rules

┌─────────────────────────────────────────────────────────────────┐
│                         PROJECTS                                │
│  (Can reference everything: components, bases, libraries)       │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                          BASES                                  │
│  (Can reference: components, libraries)                         │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                       COMPONENTS                                │
│  (Can reference: own projects, other .Abstractions,             │
│   other .ApiClient, libraries)                                  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                        LIBRARIES                                │
│  (Can reference: other libraries, BCL only)                     │
│  (CANNOT reference: components, bases, projects)                │
└─────────────────────────────────────────────────────────────────┘

Analyzer Rules

New diagnostic rules enforce library constraints:

Rule Severity Description
ACSIS0080 Error Library cannot reference component projects
ACSIS0081 Error Library cannot use Entity Framework Core
ACSIS0082 Error Library cannot use ASP.NET Core
ACSIS0083 Warning Library should not use HttpClient directly
ACSIS0084 Error Library cannot reference .Database projects
ACSIS0085 Error Library cannot reference .ApiClient projects

The existing AbstractionReferenceRequirementAnalyzer will be updated to:

  1. Recognize Acsis.Dynaplex.Strata.* as a valid reference target
  2. Maintain backward compatibility with legacy whitelisted projects during migration

Governance

Creating a new library requires:

  1. Verification that ALL qualification criteria are met
  2. Architecture team review and approval
  3. Documentation of purpose in this ADR's appendix

PR checklist for library changes:

  • Meets all qualification criteria
  • No forbidden dependencies
  • Unit tests for public API
  • XML documentation complete

Periodic review (quarterly):

  • Size audit: Flag libraries with >50 public types
  • Dependency audit: Verify no inappropriate dependencies crept in
  • Usage audit: Confirm library is still used by ≥2 consumers

Consequences

Positive

Formalizes existing practice: The whitelisted projects already behave as libraries; this makes them official

Clear governance: Explicit criteria prevent the library concept from becoming a dumping ground

Analyzer enforcement: Compile-time errors prevent libraries from accumulating inappropriate dependencies

Simplified mental model: "Library" is a well-understood concept in .NET; easier for new developers

Honest architecture: Acknowledges that some code genuinely should be shared directly

Reduced whitelist complexity: Instead of ad-hoc exceptions, a principled pattern

Better discoverability: libraries/ folder makes shared code easy to find

Negative

⚠️ One more concept: Developers must understand Component vs Library distinction

⚠️ Analyzer complexity: Additional rules to maintain and test

⚠️ Migration effort: Existing projects need to be moved/renamed

⚠️ Governance overhead: Architecture review required for new libraries

⚠️ Potential abuse: Despite safeguards, developers may try to shortcut component creation

Risks and Mitigations

Risk Likelihood Impact Mitigation
Developers put domain logic in libraries Medium High Analyzer rules + "workflow test" + PR review
Libraries grow too large Medium Medium Size audit + periodic review
Migration breaks builds Low High Incremental migration, keep legacy whitelist
Concept adds confusion Low Medium Clear documentation + examples

Trade-offs Accepted

  1. Complexity vs Honesty: We accept slightly more architectural complexity in exchange for honestly representing how code is shared
  2. Governance vs Velocity: We accept slower library creation in exchange for maintaining architectural integrity
  3. Rules vs Flexibility: We accept strict analyzer rules in exchange for preventing architecture erosion

Implementation

Phase 1: Foundation

  • Create this ADR
  • Create libraries/ directory structure
  • Update ADR index

Phase 2: Analyzer Updates

  • Update AbstractionReferenceRequirementAnalyzer with library detection
  • Create LibraryConstraintAnalyzer with ACSIS0080-0085 rules
  • Add comprehensive tests

Phase 3: Documentation

  • Update dynaplex-architecture.md with Library concept
  • Update component-patterns.md with extraction guidance
  • Create how-to/create-library.md guide

Phase 4: Migration (Incremental)

  • New libraries use Acsis.Dynaplex.Strata.* naming immediately
  • Existing whitelisted projects migrated opportunistically
  • Legacy whitelist maintained for backward compatibility

Appendix A: Project Classification

Runtime Libraries (This ADR)

These projects are or will be runtime libraries under Acsis.Dynaplex.Strata.*:

Library Purpose Status
Acsis.Encoding.Gs1 GS1 barcode encoding/decoding External NuGet (to migrate to Acsis.Dynaplex.Strata.Gs1)

Development Tooling (NOT Libraries)

These projects are development tooling under Acsis.Dynaplex.* in strata/:

Project Purpose Location
Acsis.Dynaplex Core infrastructure utilities, DbContext base, schema registry strata/core/
Acsis.Dynaplex.Strata.ServiceDefaults Aspire service configuration defaults strata/service-defaults/
Acsis.Dynaplex.Strata.Orchestration Component registration and orchestration helpers strata/orchestration/
Acsis.Dynaplex.Strata.SourceGenerators Source generators for Dynaplex patterns strata/source-generators/
Acsis.Dynaplex.RoslynAnalyzers Roslyn analyzers for architecture enforcement strata/analyzers/
Acsis.Dynaplex.EntLibCompatShim Enterprise Library compatibility layer strata/ent-lib-compat-shim/

Note: Development tooling is whitelisted in analyzers but is NOT a library—it uses the Acsis.Dynaplex.* namespace to clearly distinguish it from runtime code.

Appendix B: Decision Matrix

Use this matrix to determine if code belongs in a library or component:

START
  │
  ▼
Does the code need to own data (database schema)?
  │
  ├─ YES ──► COMPONENT (.Database project)
  │
  NO
  │
  ▼
Does the code need to expose HTTP endpoints?
  │
  ├─ YES ──► COMPONENT (service project)
  │
  NO
  │
  ▼
Does the code implement business workflows or domain policies?
  │
  ├─ YES ──► COMPONENT (.Abstractions + service)
  │
  NO
  │
  ▼
Does the code call other services (HTTP, events)?
  │
  ├─ YES ──► COMPONENT (service project)
  │
  NO
  │
  ▼
Is the code used by only ONE component?
  │
  ├─ YES ──► Keep in that component's .Abstractions
  │
  NO (used by 2+ components)
  │
  ▼
Is the code pure/stateless (no I/O, no side effects)?
  │
  ├─ NO ──► COMPONENT (reconsider design)
  │
  YES
  │
  ▼
══════════════════════════
   ✓ LIBRARY CANDIDATE
══════════════════════════
  • ADR-001: Polylith Architecture Adoption (original inspiration; libraries are similar to Polylith components)
  • ADR-004: Roslyn Analyzer Enforcement (mechanism for enforcing library constraints)
  • ADR-007: Aspire Microservices Migration (established component model that libraries complement)
  • ADR-033: Base Orchestration Pattern (introduced shared infrastructure concept)
  • ADR-036: Database Project Separation (similar pattern of formalizing project types)

References