Documentation

fsds/library-concept-implementation-plan.md

Library Concept Implementation Plan

Status: ✅ Implemented (Phases 1-3)
Created: 2025-12-17
Completed: 2025-12-17
Effort Estimate: M (3-5 days)
Related ADR: ADR-046

Executive Summary

This plan introduces a formal "Library" project type to the Dynaplex architecture. Libraries fill the space between components (services) and raw shared code—they provide stateless, pure, cross-cutting utilities that can be directly referenced without requiring HTTP communication.

Key insight: We already have libraries in practice (Acsis.Encoding.Gs1, Acsis.EntLibCompatShim, Acsis.Dynaplex.*). They're currently managed via an ad-hoc whitelist in the Roslyn analyzer. This plan formalizes them as a governed architectural concept.


Phase 1: Foundation (Day 1)

1.1 Create ADR-046: Library Projects in Dynaplex

File: docs/adrs/046-library-projects.md

Content Structure:

# ADR-046: Library Projects in Dynaplex

## Status
Accepted

## Context
The Dynaplex architecture distinguishes between:
- **Components**: Independent microservices (4-project pattern)
- **Projects**: Deployment compositions
- **Bases**: Orchestration helpers

However, some code doesn't fit the component model:
- Technical utilities (GS1 encoding, validation helpers)
- Shared domain primitives (strongly-typed IDs, value objects)
- Infrastructure abstractions (Dynaplex core, ServiceDefaults)

Currently, these are handled via an ad-hoc whitelist in `AbstractionReferenceRequirementAnalyzer`.
This creates confusion and lacks governance.

## Decision
Introduce a formal **Library** project type with:
1. Dedicated `libraries/` folder structure
2. `Acsis.Dynaplex.Strata.*` naming convention
3. Analyzer-enforced constraints
4. Clear criteria for what qualifies as a library

### What Libraries ARE
- Pure or near-pure code (no I/O, no side effects)
- Cross-cutting utilities used by ≥2 components
- Shared domain primitives (PTIDs, value objects, units)
- Technical helpers (encoding, parsing, validation)
- Stable, rarely-changing code

### What Libraries ARE NOT
- Business workflows or domain logic
- Code that owns persistence or endpoints
- Code that calls other services
- Code specific to a single component

### Qualification Criteria (ALL must be true)
1. **No persistence, no endpoints**: No DbContext, no Program.cs, no HTTP
2. **Pure or near-pure logic**: Deterministic, side-effect-free
3. **Cross-cutting**: Used by ≥2 components or components + bases
4. **Stable semantics**: Changes rarely, coordinated globally
5. **No business workflows**: No domain policies, no orchestration

### Forbidden Dependencies (enforced by analyzer)
Libraries CANNOT reference:
- `Acsis.Dynaplex.Engines.*` (any component project)
- `.Database` projects
- `.ApiClient` projects
- `Microsoft.EntityFrameworkCore.*`
- `Microsoft.AspNetCore.*`
- `System.Net.Http.HttpClient` (direct usage)

Libraries CAN reference:
- Other `Acsis.Dynaplex.Strata.*` projects
- BCL/System namespaces
- Logging abstractions (`Microsoft.Extensions.Logging.Abstractions`)
- Configuration abstractions (`Microsoft.Extensions.Options`)

## Consequences

### Positive
✅ Formalizes existing practice (GS1, EntLib shim, Dynaplex)
✅ Clear governance and criteria
✅ Analyzer enforcement prevents misuse
✅ Simplifies the analyzer whitelist
✅ Better onboarding—"library" is a known concept

### Negative
⚠️ One more concept to teach (Component vs Library)
⚠️ Requires analyzer updates
⚠️ Migration effort for existing projects
⚠️ Ongoing governance burden

## Migration
Existing whitelisted projects migrate to `libraries/` structure over time.
New libraries require architecture review.

1.2 Update ADR Index

File: docs/adrs/README.md

Add to the ADR table:

| [046](046-library-projects.md) | Library Projects in Dynaplex | Accepted |

Phase 2: Folder Structure (Day 1-2)

2.1 Create Libraries Root Directory

mkdir -p libraries
touch libraries/.keep

2.2 Define Standard Structure

libraries/
├── encoding/
│   └── src/
│       └── Acsis.Dynaplex.Strata.Gs1/
│           ├── Acsis.Dynaplex.Strata.Gs1.csproj
│           └── [code files]
│
├── domain-primitives/
│   └── src/
│       └── Acsis.Dynaplex.Strata.DomainPrimitives/
│           ├── Acsis.Dynaplex.Strata.DomainPrimitives.csproj
│           └── [PTIDs, value objects, etc.]
│
├── validation/
│   └── src/
│       └── Acsis.Dynaplex.Strata.Validation/
│           └── [validation helpers]
│
├── compatibility/
│   └── src/
│       └── Acsis.Dynaplex.Strata.EntLibCompat/
│           └── [Enterprise Library shim]
│
└── infrastructure/
    └── src/
        ├── Acsis.Dynaplex.Strata.Dynaplex/
        ├── Acsis.Dynaplex.Strata.Dynaplex.ServiceDefaults/
        └── Acsis.Dynaplex.Strata.Dynaplex.Orchestration/

2.3 Naming Convention

Type Pattern Example
Library Folder kebab-case domain-primitives
Project Name Acsis.Dynaplex.Strata.{Name} Acsis.Dynaplex.Strata.Gs1
Namespace Acsis.Dynaplex.Strata.{Name} Acsis.Dynaplex.Strata.Gs1

Phase 3: Analyzer Updates (Day 2-3)

3.1 Update AbstractionReferenceRequirementAnalyzer

File: strata/analyzers/src/Acsis.RoslynAnalyzers/AbstractionReferenceRequirementAnalyzer.cs

Current:

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

Updated:

private static bool IsLibraryProject(string assemblyName) {
    // New library namespace pattern
    if (assemblyName.StartsWith("Acsis.Dynaplex.Strata.", StringComparison.Ordinal)) {
        return true;
    }

    // Legacy libraries (until migrated)
    return assemblyName is
        "Acsis.Dynaplex" or
        "Acsis.Dynaplex.Strata.ServiceDefaults" or
        "Acsis.Dynaplex.Strata.Orchestration" or
        "Acsis.EntLibCompatShim" or
        "Acsis.Encoding.Gs1" or
        "Acsis.RoslynAnalyzers";
}

// In the analysis logic:
if (IsLibraryProject(referencedName)) {
    return; // Libraries can be referenced by anyone
}

3.2 Create New Analyzer: LibraryConstraintAnalyzer

File: strata/analyzers/src/Acsis.RoslynAnalyzers/LibraryConstraintAnalyzer.cs

Purpose: Enforce that library projects don't contain forbidden dependencies.

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class LibraryConstraintAnalyzer : DiagnosticAnalyzer {

    // ACSIS0080: Library projects cannot reference component projects
    // ACSIS0081: Library projects cannot use Entity Framework Core
    // ACSIS0082: Library projects cannot use ASP.NET Core
    // ACSIS0083: Library projects cannot use HttpClient directly
    // ACSIS0084: Library projects cannot reference .Database projects
    // ACSIS0085: Library projects cannot reference .ApiClient projects

    private static readonly string[] ForbiddenNamespacePrefixes = [
        "Microsoft.EntityFrameworkCore",
        "Microsoft.AspNetCore",
    ];

    private static readonly string[] ForbiddenAssemblyPatterns = [
        ".Database",
        ".ApiClient",
        "Acsis.Dynaplex.Engines."
    ];

    private static bool IsLibraryProject(string assemblyName) =>
        assemblyName.StartsWith("Acsis.Dynaplex.Strata.", StringComparison.Ordinal);

    public override void Initialize(AnalysisContext context) {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

        context.RegisterCompilationStartAction(ctx => {
            var assemblyName = ctx.Compilation.Assembly.Name;

            // Only analyze library projects
            if (!IsLibraryProject(assemblyName)) {
                return;
            }

            // Check referenced assemblies
            foreach (var reference in ctx.Compilation.ReferencedAssemblyNames) {
                CheckForbiddenReference(ctx, reference.Name);
            }

            // Register operation checks for namespace usage
            ctx.RegisterOperationAction(CheckForbiddenNamespace,
                OperationKind.Invocation,
                OperationKind.ObjectCreation,
                OperationKind.PropertyReference);
        });
    }

    private static void CheckForbiddenReference(CompilationStartAnalysisContext ctx, string refName) {
        foreach (var pattern in ForbiddenAssemblyPatterns) {
            if (refName.Contains(pattern, StringComparison.Ordinal)) {
                // Report diagnostic at assembly level
            }
        }
    }

    private static void CheckForbiddenNamespace(OperationAnalysisContext ctx) {
        var containingNamespace = GetContainingNamespace(ctx.Operation);
        foreach (var prefix in ForbiddenNamespacePrefixes) {
            if (containingNamespace?.StartsWith(prefix, StringComparison.Ordinal) == true) {
                ctx.ReportDiagnostic(/* ... */);
            }
        }
    }
}

3.3 Add Analyzer Definitions

File: strata/analyzers/src/Acsis.RoslynAnalyzers/AnalyzerDefinitions.cs

Add new diagnostic definitions:

public static readonly DiagnosticDescriptor LibraryReferencesComponent = new(
    "ACSIS0080",
    "Library references component project",
    "Library '{0}' cannot reference component project '{1}'. Libraries must remain independent of components.",
    "Architecture",
    DiagnosticSeverity.Error,
    isEnabledByDefault: true);

public static readonly DiagnosticDescriptor LibraryUsesEntityFramework = new(
    "ACSIS0081",
    "Library uses Entity Framework Core",
    "Library '{0}' cannot use Entity Framework Core. Libraries must be pure and not own persistence.",
    "Architecture",
    DiagnosticSeverity.Error,
    isEnabledByDefault: true);

public static readonly DiagnosticDescriptor LibraryUsesAspNetCore = new(
    "ACSIS0082",
    "Library uses ASP.NET Core",
    "Library '{0}' cannot use ASP.NET Core. Libraries cannot expose endpoints.",
    "Architecture",
    DiagnosticSeverity.Error,
    isEnabledByDefault: true);

public static readonly DiagnosticDescriptor LibraryUsesHttpClient = new(
    "ACSIS0083",
    "Library uses HttpClient directly",
    "Library '{0}' cannot use HttpClient directly. Libraries must be pure and not make HTTP calls.",
    "Architecture",
    DiagnosticSeverity.Warning,  // Warning, not error—some edge cases may be valid
    isEnabledByDefault: true);

public static readonly DiagnosticDescriptor LibraryReferencesDatabase = new(
    "ACSIS0084",
    "Library references Database project",
    "Library '{0}' cannot reference Database project '{1}'. Libraries must be independent of data access.",
    "Architecture",
    DiagnosticSeverity.Error,
    isEnabledByDefault: true);

public static readonly DiagnosticDescriptor LibraryReferencesApiClient = new(
    "ACSIS0085",
    "Library references ApiClient project",
    "Library '{0}' cannot reference ApiClient project '{1}'. Libraries cannot depend on component APIs.",
    "Architecture",
    DiagnosticSeverity.Error,
    isEnabledByDefault: true);

3.4 Add Analyzer Tests

File: strata/analyzers/test/Acsis.RoslynAnalyzers.Tests/LibraryConstraintAnalyzerTests.cs

public class LibraryConstraintAnalyzerTests {

    [Fact]
    public void Library_CannotReference_ComponentProject() { /* ... */ }

    [Fact]
    public void Library_CannotUse_EntityFramework() { /* ... */ }

    [Fact]
    public void Library_CannotUse_AspNetCore() { /* ... */ }

    [Fact]
    public void Library_CanReference_OtherLibrary() { /* ... */ }

    [Fact]
    public void Library_CanReference_SystemNamespaces() { /* ... */ }

    [Fact]
    public void NonLibrary_IsNotAffected() { /* ... */ }
}

Phase 4: Documentation Updates (Day 3-4)

4.1 Update Dynaplex Architecture Doc

File: docs/reference/patterns/dynaplex-architecture.md

Add new section after "Component Structure":

## Library Structure

In addition to components, Dynaplex includes **libraries**—pure, stateless code that can be directly referenced.

### What are Libraries?

Libraries provide cross-cutting utilities and shared primitives:

- **Technical utilities**: GS1 encoding, parsing, validation helpers
- **Domain primitives**: Strongly-typed IDs (PTIDs), value objects, units
- **Infrastructure abstractions**: Dynaplex core utilities

### Library vs Component

| Aspect | Library | Component |
|--------|---------|-----------|
| Deployment | Not deployed independently | Independent microservice |
| Communication | Direct reference | HTTP via ApiClient |
| Persistence | Cannot own data | Owns database schema |
| Structure | Single project | 4-project pattern |
| Purpose | Pure utilities | Business logic + APIs |

### Library Structure

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


### Qualification Criteria

A project should be a library when ALL are true:
1. No persistence, no endpoints
2. Pure or near-pure logic
3. Cross-cutting (used by ≥2 components)
4. Stable semantics
5. No business workflows

See [ADR-046](../../adrs/046-library-projects.md) for complete details.

4.2 Update Component Patterns Doc

File: docs/reference/patterns/component-patterns.md

Add section:

## When to Extract to a Library

Consider extracting code to a library when:

✅ **Good candidates:**
- Parsing/encoding logic (GS1, barcodes)
- Validation attributes and helpers
- Strongly-typed IDs and value objects
- Unit conversion utilities
- Extension methods for BCL types

❌ **Keep in component:**
- Business rules specific to one domain
- Code that needs database access
- Code that calls other services
- Orchestration or workflow logic

### Example: Extracting Shared Validation

**Before** (duplicated in multiple components):
```csharp
// In CoreData.Abstractions
public static class ValidationHelpers {
    public static bool IsValidBarcode(string barcode) { /* ... */ }
}

// In Catalog.Abstractions (copy-pasted)
public static class ValidationHelpers {
    public static bool IsValidBarcode(string barcode) { /* ... */ }
}

After (extracted to library):

// In Acsis.Dynaplex.Strata.Validation
namespace Acsis.Dynaplex.Strata.Validation;

public static class BarcodeValidation {
    public static bool IsValid(string barcode) { /* ... */ }
}

Both components now reference Acsis.Dynaplex.Strata.Validation.


### 4.3 Update Analyzer Rules Doc

**File:** `docs/reference/analyzer-rules.md`

Add library-related rules:

```markdown
## Library Constraint Rules

These rules enforce that library projects remain pure and independent.

| 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 |

### How to Fix

**ACSIS0080/0084/0085**: The code you're adding belongs in a component, not a library. Either:
- Move the code to the appropriate component's `.Abstractions` project
- Create a new component if the logic needs its own service

**ACSIS0081/0082**: Libraries cannot own persistence or expose endpoints. This code belongs in:
- A component's main service project (for endpoints)
- A component's `.Database` project (for EF Core)

**ACSIS0083**: If you need HTTP communication, this belongs in a component or should use an injected abstraction.

4.4 Create Library Development Guide

File: docs/how-to/create-library.md

# How to Create a Library

This guide covers creating a new library project in the Dynaplex architecture.

## Prerequisites

- Architecture team approval (libraries require review)
- Clear use case matching [library criteria](../adrs/046-library-projects.md)

## Steps

### 1. Create Directory Structure

```bash
mkdir -p libraries/{library-name}/src/Acsis.Dynaplex.Strata.{LibraryName}

2. Create Project File

File: libraries/{library-name}/src/Acsis.Dynaplex.Strata.{LibraryName}/Acsis.Dynaplex.Strata.{LibraryName}.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>Acsis.Dynaplex.Strata.$(MSBuildProjectName.Replace("Acsis.Dynaplex.Strata.", ""))</RootNamespace>
    <IsLibrary>true</IsLibrary>
  </PropertyGroup>

  <!--
    Libraries should have MINIMAL dependencies.
    Only add what you truly need.
  -->
  <ItemGroup>
    <!-- Example: Logging abstractions (if needed) -->
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
  </ItemGroup>
</Project>

3. Add to Solution

dotnet sln acsis-core.slnx add libraries/{library-name}/src/Acsis.Dynaplex.Strata.{LibraryName}/Acsis.Dynaplex.Strata.{LibraryName}.csproj

4. Create Initial Code

Follow these guidelines:

  • Keep classes static or sealed where appropriate
  • Use readonly and immutability
  • Avoid mutable state
  • Write comprehensive XML documentation
  • Include unit tests

5. Document in ADR-046

Add your library to the "Current Libraries" table in ADR-046.

Library Template Checklist

  • Single responsibility—library does one thing well
  • No EF Core, ASP.NET Core, or HttpClient
  • No references to component projects
  • Comprehensive unit tests
  • XML documentation on public APIs
  • Added to development solution
  • Architecture review completed

---

## Phase 5: Migration Plan (Day 4-5)

### 5.1 Existing Projects to Migrate

| Current Location | Current Name | New Name | Priority |
|-----------------|--------------|----------|----------|
| External NuGet | `Acsis.Encoding.Gs1` | `Acsis.Dynaplex.Strata.Gs1` | P2 (keep NuGet for now) |
| `strata/ent-lib-compat-shim` | `Acsis.EntLibCompatShim` | `Acsis.Dynaplex.Strata.EntLibCompat` | P3 |
| `strata/core` | `Acsis.Dynaplex` | `Acsis.Dynaplex.Strata.Dynaplex` | P1 |
| `strata/service-defaults` | `Acsis.Dynaplex.Strata.ServiceDefaults` | `Acsis.Dynaplex.Strata.Dynaplex.ServiceDefaults` | P1 |
| `strata/orchestration` | `Acsis.Dynaplex.Strata.Orchestration` | `Acsis.Dynaplex.Strata.Dynaplex.Orchestration` | P1 |

### 5.2 Migration Strategy

**Option A: Big Bang** (Not recommended)
- Rename all projects at once
- High risk, complex rollback

**Option B: Incremental with Aliases** (Recommended)
1. Create new library structure
2. Add forwarding/aliasing from old names
3. Update references component by component
4. Remove old projects when all references updated

**Option C: New Libraries Only** (Fastest initial)
1. New libraries use `Acsis.Dynaplex.Strata.*` naming
2. Existing projects stay in place with legacy whitelist
3. Migrate existing projects opportunistically

**Recommendation:** Start with **Option C**, migrate existing projects as part of related work.

### 5.3 New Library Candidates

Based on codebase analysis, these should be considered for new libraries:

| Candidate | Source | Rationale |
|-----------|--------|-----------|
| `Acsis.Dynaplex.Strata.DomainPrimitives` | Various Abstractions | Consolidate PTIDs, PassportId, common value objects |
| `Acsis.Dynaplex.Strata.Validation` | Various | Consolidate validation attributes and helpers |
| `Acsis.Dynaplex.Strata.Mapping` | Various | Consolidate DTO mapping extensions |

---

## Phase 6: Governance Process

### 6.1 New Library Request Process

1. **Proposal**: Developer identifies need for shared code
2. **Criteria Check**: Does it meet ALL library criteria?
3. **Review**: Architecture team reviews proposal
4. **Approval/Rejection**: Decision with rationale
5. **Implementation**: Follow `create-library.md` guide

### 6.2 PR Checklist for Library Changes

Add to PR template:

```markdown
## Library Changes

If this PR modifies or creates a library project:
- [ ] Meets all criteria in ADR-046
- [ ] No forbidden dependencies (EF Core, ASP.NET, components)
- [ ] Unit tests added/updated
- [ ] XML documentation complete
- [ ] Architecture team approved (for new libraries)

6.3 Periodic Review

Quarterly review of all libraries:

  • Size check: Is any library growing too large (>50 public types)?
  • Dependency check: Are libraries accumulating inappropriate dependencies?
  • Usage check: Is the library still used by ≥2 components?

Implementation Checklist

Day 1: Foundation ✅

  • Create ADR-046 draft
  • Create libraries/ directory structure
  • Update ADR README

Day 2: Analyzers ✅

  • Update AbstractionReferenceRequirementAnalyzer with library detection
  • Create LibraryConstraintAnalyzer
  • Add analyzer definitions
  • Write analyzer tests

Day 3: Testing & Refinement ✅

  • Run full solution build
  • Verify analyzers work correctly
  • Fix any false positives

Day 4: Documentation ✅

  • Update dynaplex-architecture.md
  • Update component-patterns.md
  • Update analyzer-rules.md
  • Create create-library.md

Day 5: Governance & Rollout (Future)

  • Update PR template
  • Team communication
  • Consider first new library candidate

Success Criteria

  1. Analyzer enforcement works: Building a library with EF Core reference fails
  2. Documentation complete: New developers can understand the concept
  3. Legacy projects still work: Existing whitelist remains functional
  4. First library created: At least one new Acsis.Dynaplex.Strata.* project exists

Risks and Mitigations

Risk Likelihood Impact Mitigation
Developers abuse libraries for domain logic Medium High Strong analyzer rules + PR review
Migration breaks existing builds Low High Incremental migration, keep legacy whitelist
Concept adds confusion Low Medium Clear documentation + examples
Libraries become bloated Medium Medium Periodic review + size warnings

Future Considerations

  1. Library versioning: If libraries need independent versioning, consider internal NuGet feed
  2. Cross-repo sharing: If libraries need to be shared across repositories, extract to separate repo
  3. Advanced categories: May later split into Acsis.Dynaplex.Strata.Technical.* and Acsis.Dynaplex.Strata.Domain.*