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
staticorsealedwhere appropriate - Use
readonlyand 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
AbstractionReferenceRequirementAnalyzerwith 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
- Analyzer enforcement works: Building a library with EF Core reference fails
- Documentation complete: New developers can understand the concept
- Legacy projects still work: Existing whitelist remains functional
- 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
- Library versioning: If libraries need independent versioning, consider internal NuGet feed
- Cross-repo sharing: If libraries need to be shared across repositories, extract to separate repo
- Advanced categories: May later split into
Acsis.Dynaplex.Strata.Technical.*andAcsis.Dynaplex.Strata.Domain.*