This document covers the architecture of the semantic strings, paths, and validation subsystems. The semantic quantities subsystem is metadata-driven and is documented separately:
docs/strategy-unified-vector-quantities.md— the unifiedIVector0..IVector4model.docs/physics-generator.md—dimensions.jsonschema and the source-generator pipeline.- The Semantic Quantities: Metadata-Driven Generation section below provides a short orientation and links into those documents.
- Overview
- SOLID Principles Implementation
- DRY Implementation
- Design Patterns
- Class Hierarchy
- Validation System
- Semantic Quantities: Metadata-Driven Generation
- Testing Strategy
The Semantics library is built around three pillars — semantic strings, semantic paths, and semantic quantities — sharing a single philosophy: replace primitive obsession with strongly-typed, self-validating domain models. Strings and paths use a hand-authored attribute → strategy → rule → factory pipeline (described in this document). Semantic quantities are emitted at compile time by a Roslyn incremental generator from declarative metadata (described in Semantic Quantities: Metadata-Driven Generation). Strings and paths target net8.0–net10.0 plus netstandard2.0/netstandard2.1; semantic quantities target net8.0–net10.0.
Each class has a single, well-defined responsibility:
ISemanticStringFactory<T>: Object creation onlySemanticStringFactory<T>: Concrete creation logic- Purpose: Separates construction from business logic
IValidationStrategy: Defines validation processingValidateAllStrategy: "All must pass" logicValidateAnyStrategy: "Any can pass" logicValidationStrategyFactory: Strategy creation
Open for extension, closed for modification:
// Add new validation rules without modifying existing code
public class CustomBusinessRule : ValidationRuleBase
{
public override string RuleName => "CustomBusiness";
protected override bool ValidateCore(SemanticStringValidationAttribute attribute, ISemanticString value)
{
// Custom validation logic
return true;
}
}All implementations are fully substitutable through behavioral contracts:
// Contract validation ensures LSP compliance
public static class SemanticStringContracts
{
public static bool ValidateContracts<T>(T instance) where T : SemanticString<T>
{
// Validates basic contracts (reflexivity, etc.)
}
public static bool ValidateEqualityContracts<T>(T? first, T? second) where T : SemanticString<T>
{
// Validates equality behavior
}
}Focused, client-specific interfaces:
ISemanticStringFactory<T>: Factory operations onlyIValidationStrategy: Validation strategy operations onlyIValidationRule: Individual rule operations only
High-level modules depend on abstractions:
// Service depends on abstraction, not concrete implementation
public class UserService
{
private readonly ISemanticStringFactory<EmailAddress> _emailFactory;
public UserService(ISemanticStringFactory<EmailAddress> emailFactory)
{
_emailFactory = emailFactory;
}
}Problem Eliminated: Duplicated validation logic across multiple methods.
Solution: Centralized validation strategies with pluggable implementations.
public interface IValidationStrategy
{
bool Validate(IEnumerable<SemanticStringValidationAttribute> attributes, ISemanticString value);
}
// Reusable strategies eliminate duplication
public class ValidateAllStrategy : IValidationStrategy { /* implementation */ }
public class ValidateAnyStrategy : IValidationStrategy { /* implementation */ }Problem Eliminated: Common validation patterns repeated across attribute types.
Solution: Base class with template method for common functionality.
public abstract class ValidationRuleBase : IValidationRule
{
// Template method - common structure, specific implementation in derived classes
public bool Validate(SemanticStringValidationAttribute attribute, ISemanticString value)
{
if (!IsApplicable(attribute)) return false;
if (value?.ToString() is not string stringValue) return false;
return ValidateCore(attribute, value); // Specific implementation
}
protected abstract bool ValidateCore(SemanticStringValidationAttribute attribute, ISemanticString value);
}- Purpose: Encapsulate object creation logic
- Implementation:
ISemanticStringFactory<T>andSemanticStringFactory<T> - Benefits: Consistent creation, testability, DI support
- Purpose: Interchangeable validation algorithms
- Implementation:
IValidationStrategywith concrete strategies - Benefits: Extensibility, configurable behavior
- Purpose: Define algorithm structure with customizable steps
- Implementation:
ValidationRuleBasewith abstract methods - Benefits: Code reuse, consistent structure
ISemanticString
├── SemanticString<TDerived> (abstract base)
├── SemanticPath<TDerived> (path-specific base)
│ ├── SemanticAbsolutePath<TDerived> (absolute paths base)
│ │ └── AbsolutePath
│ ├── SemanticRelativePath<TDerived> (relative paths base)
│ │ └── RelativePath
│ ├── SemanticFilePath<TDerived> (file paths base)
│ │ ├── FilePath
│ │ ├── AbsoluteFilePath
│ │ └── RelativeFilePath
│ ├── SemanticDirectoryPath<TDerived> (directory paths base)
│ │ ├── DirectoryPath
│ │ ├── AbsoluteDirectoryPath
│ │ └── RelativeDirectoryPath
│ ├── FileName
│ └── FileExtension
└── [Custom semantic string types]
ISemanticStringFactory<T>
└── SemanticStringFactory<T>
IValidationStrategy
├── ValidateAllStrategy
├── ValidateAnyStrategy
└── [Custom validation strategies]
IValidationRule
├── ValidationRuleBase (abstract)
│ ├── LengthValidationRule
│ ├── PatternValidationRule
│ └── [Custom validation rules]
└── [Custom validation rules]
The library provides a comprehensive interface hierarchy for path types that enables polymorphism and type-safe operations:
IPath : IComparable<IPath> (base interface for all path types; exposes WeakString)
├── IAbsolutePath : IPath
├── IRelativePath : IPath
├── IFilePath : IPath
├── IDirectoryPath : IPath
├── IAbsoluteFilePath : IFilePath, IAbsolutePath
├── IRelativeFilePath : IFilePath, IRelativePath
├── IAbsoluteDirectoryPath : IDirectoryPath, IAbsolutePath
└── IRelativeDirectoryPath : IDirectoryPath, IRelativePath
IFileName (separate hierarchy for non-path file components; exposes WeakString)
IFileExtension (separate hierarchy for file extensions; exposes WeakString)
IDirectoryName (separate hierarchy for directory names; exposes WeakString)
Every interface in these hierarchies exposes WeakString, so polymorphic code — most importantly the
IEnumerable<IPath> returned by GetContents() — can read a path's value without downcasting to a
concrete type.
IPath also declares IComparable<IPath>, which lets Comparer<IPath>.Default sort a collection of
paths through the generic comparison path. SemanticPath<TDerived> implements it explicitly: a
public overload taking IPath would make pathA.CompareTo(pathB) ambiguous against the inherited
overload taking ISemanticString, because a concrete path satisfies both parameter types and neither
is more specific.
Interface Implementation Mapping:
// Path types implement their corresponding interfaces
AbsolutePath : SemanticAbsolutePath<AbsolutePath>, IAbsolutePath
RelativePath : SemanticRelativePath<RelativePath>, IRelativePath
FilePath : SemanticFilePath<FilePath>, IFilePath
DirectoryPath : SemanticDirectoryPath<DirectoryPath>, IDirectoryPath
AbsoluteFilePath : SemanticFilePath<AbsoluteFilePath>, IAbsoluteFilePath
RelativeFilePath : SemanticFilePath<RelativeFilePath>, IRelativeFilePath
AbsoluteDirectoryPath : SemanticDirectoryPath<AbsoluteDirectoryPath>, IAbsoluteDirectoryPath
RelativeDirectoryPath : SemanticDirectoryPath<RelativeDirectoryPath>, IRelativeDirectoryPath
// Non-path types have separate interfaces
FileName : SemanticString<FileName>, IFileName
FileExtension : SemanticString<FileExtension>, IFileExtensionPolymorphic Benefits:
- Type-safe Collections: Store different path types in the same collection using common interfaces
- Polymorphic Methods: Write methods that accept any path type or specific categories
- Interface Segregation: Use the most specific interface needed for your use case
- Extensibility: Easy to add new path types that integrate with existing polymorphic code
The validation system follows a layered approach:
- Attribute Layer:
SemanticStringValidationAttributeclasses - Strategy Layer:
IValidationStrategyimplementations - Rule Layer:
IValidationRuleimplementations - Factory Layer:
ValidationStrategyFactorycreates strategies
User Creates Semantic String
↓
SemanticStringFactory
↓
Get Validation Attributes
↓
ValidationStrategyFactory.GetStrategy()
↓
IValidationStrategy.Validate()
↓
For Each Attribute: IValidationRule.Validate()
↓
Combine Results (All/Any/Custom)
↓
Return Valid Object or Throw Exception
Unlike strings and paths — which are hand-authored — every semantic quantity type, factory, operator, and constant is emitted by a Roslyn incremental generator. The single source of truth is Semantics.SourceGenerators/Metadata/:
| File | Contents |
|---|---|
dimensions.json |
Every physical dimension, the vector forms it supports (Vector0..Vector4), its availableUnits, semantic overloads (e.g. Weight over ForceMagnitude), and cross-dimensional relationships (integrals, derivatives, dotProducts, crossProducts). |
units.json |
Unit declarations (singular-lemma name, used verbatim as the From{Name} factory suffix) and a base-unit conversion expression. |
magnitudes.json |
SI magnitude prefixes for unit derivations. |
conversions.json |
Conversion factors between non-SI units and the SI base. |
domains.json |
Domain grouping for PhysicalConstants (e.g. Fundamental, Chemistry, AngularMechanics). |
Metadata/*.json
│
▼
Semantics.SourceGenerators (Roslyn IIncrementalGenerator)
│
├── QuantitiesGenerator → one record per quantity (V0/V1/V2/V3/V4 + overloads)
│ + From{Unit} factory per declared unit
│ + Vector0Guards.EnsureNonNegative / EnsurePositive
│ + cross-dimensional *, /, Dot, Cross operators
├── ConversionsGenerator → unit-to-SI conversion helpers
├── PhysicalConstantsGenerator → PhysicalConstants.<Domain>.*<T>() (T.Parse, cached per T)
│ + PhysicalConstants.Generic.*<T>() (flat accessors)
└── StorageHelpersGenerator → DivideToStorage with DivideByZeroException
│
▼
Semantics.Quantities/Generated/ (committed source — diff before commit)
Every generated V0 / V1 quantity (and V0/V1 semantic overload) implements
IPhysicalQuantity<TSelf, T>, and through it the slim IPhysicalQuantity<T>:
public interface IPhysicalQuantity<T>
: ISemanticQuantity<T>
, IComparable<IPhysicalQuantity<T>>
, IEquatable<IPhysicalQuantity<T>>
where T : struct, INumber<T>
{
T Value { get; } // stored in the dimension's SI base unit
bool IsPhysicallyValid { get; } // structural: finite, non-NaN
DimensionInfo Dimension { get; } // PhysicalDimensions.X (generated singleton)
}Semantics (locked in #59):
Dimensionis generated per quantity as=> PhysicalDimensions.{Name}so every instance knows what it measures without reflection.CompareTo(IPhysicalQuantity<T>?)compares stored SI-base values, but throwsArgumentExceptionwhen the dimensions differ — quantities of different dimensions are not ordered.Equals(IPhysicalQuantity<T>?)is total: cross-dimension comparisons returnfalserather than throwing, because equality (unlike ordering) must be defined for every pair.
V2 / V3 / V4 vector types implement only their IVectorN<TSelf, T> interface — the
slim IPhysicalQuantity<T> contract applies to scalar-storage quantities.
A quantity is a readonly record struct, so none of this is inherited: there is no
base class to inherit it from. IPhysicalQuantity<TSelf, T> adds
static abstract TSelf Create(T), which is what replaced the
where TSelf : PhysicalQuantity<TSelf, T>, new() constraint the record base needed, and
the generator emits the members per type. That is not merely how the surface is kept —
it is the point. The record base's Create was new TQuantity() with { Quantity = value },
two heap allocations for one number, on every operator and every unit factory; an operator
declared on the struct itself is a plain arithmetic expression the JIT inlines to nothing.
The three rules above are shared without a base class through PhysicalQuantityCore,
which each quantity delegates to in one line. QuantityValueTypeTests measures the
allocation, so the regression cannot return silently.
Each V0/V1 quantity emits a dimensionally-typed In method:
// On generated Length<T>:
public T In(ILengthUnit unit) => unit.FromBase(Value);
// On generated Temperature<T>:
public T In(ITemperatureUnit unit) => unit.FromBase(Value);ILengthUnit, ITemperatureUnit, etc. are marker interfaces generated by
DimensionsGenerator — each declared unit implements IUnit plus the marker(s) for
the dimension(s) it belongs to. Cross-dimension calls fail at compile time:
length.In(Units.Kilogram); // ❌ compile error — Kilogram : IMassUnit, not ILengthUnit
length.In(Units.Kilometer); // ✓The IUnit interface carries Name, Symbol, Dimension, and the affine conversion
(base = value × ToBaseFactor + ToBaseOffset). ToBase<T> / FromBase<T> are default
interface methods, so each concrete unit only has to declare its factor and offset.
The static Units catalogue exposes one singleton per declared unit.
UnitConversionException remains in the runtime for any future untyped-unit dispatch
path; the typed In(I{Dim}Unit) path is compile-time-safe and does not throw.
These are enforced structurally by the generated types and locked in by Semantics.Test:
V0magnitudes are non-negative; the SI factory throwsArgumentExceptionon a negative value, andV0 - V0returnsT.Abs(a - b)to preserve the invariant.- A V0 overload can opt into a strict-positive guard with
physicalConstraints: { "minExclusive": "0" }(used byWavelength,Period,HalfLife);EnsurePositivethen rejects zero as well. - Semantic overloads widen implicitly to their base, narrow explicitly (
Weight.From(forceMagnitude)). IVectorN.Magnitude()for N ≥ 1 returns the correspondingIVector0.
Metadata errors fail the build rather than silently emitting wrong code:
- SEM001 — a relationship references a dimension that does not exist.
- SEM002 — schema-level metadata issue (missing
name/symbol, emptyavailableUnits, duplicate type names, no vector forms declared). - SEM003 — a relationship's explicit
formslist references a vector form not declared on a participating dimension. - SEM004 — a dimension's
availableUnitsarray references a unit name that isn't declared inunits.json(catches typos that would otherwise produce a wrong identity-conversion factory).
For the schema, an end-to-end "add a dimension" walk-through, and the design rationale, see docs/physics-generator.md and docs/strategy-unified-vector-quantities.md.
- All implementations must pass contract validation
SemanticStringContractsprovides standardized tests- Ensures LSP compliance
[Test]
public void EmailAddress_ShouldSatisfyContracts()
{
var email1 = _factory.Create("user1@example.com");
var email2 = _factory.Create("user2@example.com");
var email3 = _factory.Create("user3@example.com");
Assert.IsTrue(SemanticStringContracts.ValidateContracts(email1));
Assert.IsTrue(SemanticStringContracts.ValidateEqualityContracts(email1, email2));
Assert.IsTrue(SemanticStringContracts.ValidateComparisonContracts(email1, email2, email3));
}This architecture ensures the library remains maintainable, extensible, and testable while providing excellent performance and type safety.