Building .NET Infrastructure That Composes
"Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new features." — Doug McIlroy, Unix Philosophy, 1978
Nine NuGet packages. Nine single-responsibility libraries. Each one solves exactly one problem. Each one ships a .Testing package with fakes, assertions, and deterministic test doubles. Each one registers itself into IServiceCollection via a single [Injectable] attribute — source-generated, zero reflection, compile-time safe.
This is not a framework in the traditional sense. There is no AddFrenchExDev() call that wires 400 services you will never use. There is no base class that forces you into an inheritance hierarchy. There are nine composable building blocks that work together because they share five design principles — not because they share a God namespace.
This series walks through all nine patterns: what problem each one solves, how the API works, how to test it, how it integrates with dependency injection, and how it composes with the others.
The Nine Patterns
| Pattern | Package | Key Types | What It Solves |
|---|---|---|---|
| Option | FrenchExDev.Net.Options |
Option<T>, OptionExtensions |
Representing absence without null |
| Union | FrenchExDev.Net.Union |
OneOf<T1,T2>, OneOf<T1,T2,T3>, OneOf<T1,T2,T3,T4> |
Exhaustive choice between N types |
| Guard | FrenchExDev.Net.Guard |
Guard.Against, Guard.ToResult, Guard.Ensure |
Input validation and invariant assertion |
| Clock | FrenchExDev.Net.Clock |
IClock, SystemClock, FakeClock |
Deterministic time abstraction |
| Mapper | FrenchExDev.Net.Mapper |
IMapper<TSource,TTarget>, [MapFrom], [MapTo] |
Source-generated, zero-reflection mapping |
| Mediator | FrenchExDev.Net.Mediator |
IMediator, ICommand<T>, IQuery<T>, IBehavior |
CQRS dispatch and pipeline middleware |
| Reactive | FrenchExDev.Net.Reactive |
IEventStream<T>, EventStream<T> |
Domain event streams with Rx operators |
| Saga | FrenchExDev.Net.Saga |
SagaOrchestrator<T>, ISagaStep<T> |
Multi-step orchestration with compensation |
| Outbox | FrenchExDev.Net.Outbox |
OutboxMessage, OutboxInterceptor |
Transactional event publishing via EF Core |
Part I: Design Philosophy
Why nine small libraries beat one big one. The five design principles that every pattern shares: composability over completeness, testing as a first-class package, source generation over reflection, Result<T> as the lingua franca, and netstandard2.0 compatibility. How [Injectable] source-generates DI registration across all nine patterns. The anatomy of a .Testing package.
Part II: The Option Pattern
Option<T> as a sealed record: Some and None, exhaustive Match/Switch, the full extension landscape — Map, Bind, Filter, Tap, OrDefault, OrElse, Zip, Contains. Async pipelines on Task<Option<T>>. LINQ query syntax. Collection extensions including Haskell-inspired Sequence and Traverse. Bidirectional Result<T> integration. Testing with OptionAssertions.
Part III: The Union Pattern
Discriminated unions in C# without language support. OneOf<T1,T2>, OneOf<T1,T2,T3>, OneOf<T1,T2,T3,T4>: byte discriminator, private storage, implicit conversions, exhaustive Match/Switch, TryGet<T>, IEquatable. When to use Union vs Option vs Result. Real-world: modeling payment methods as a closed set of types.
Part IV: The Guard Pattern
Three prongs of defensive programming. Guard.Against throws exceptions at API boundaries. Guard.ToResult returns Result<T> for functional pipelines. Guard.Ensure asserts internal invariants with InvalidOperationException. [CallerArgumentExpression] captures parameter names automatically. Every method returns the validated value for inline chaining. Fourteen built-in checks from Null to UndefinedEnum.
Part V: The Clock Pattern
IClock wrapping .NET 8+ TimeProvider with a domain-oriented API. SystemClock.Instance for production. FakeClock for testing with Advance(TimeSpan) and SetUtcNow(DateTimeOffset). UtcNow, Now(TimeZoneInfo), Today, CreateTimer, Delay. Why DateTime.UtcNow is a hidden dependency and how to eliminate it.
Part VI: The Mapper Pattern
IMapper<in TSource, out TTarget> with variance. Five attributes — [MapFrom], [MapTo], [MapProperty], [IgnoreMapping], [OneWay] — that feed a source generator at compile time. Zero reflection, zero runtime cost. What the generator emits. Three-layer mapping: Domain to DTO to Persistence Entity. Testing with MapperAssertions. Comparison with AutoMapper.
Part VII: The Mediator Pattern
IMediator with SendAsync and PublishAsync. CQRS markers: ICommand<T> and IQuery<T>. IBehavior<TRequest,TResult> pipeline middleware for logging, validation, authorization, and transactions. INotification with three publish strategies: Sequential, Parallel, FireAndForget. Testing with FakeMediator. Comparison with MediatR.
Part VIII: The Reactive Pattern
IEventStream<T> wrapping System.Reactive with domain vocabulary. EventStream<T> backed by Subject<T>. Ten operators: Filter, Map, Merge, Buffer, Throttle, DistinctUntilChanged, Take, Skip, OfType. The ObservableEventStream<T> adapter. AsObservable() escape hatch for full Rx interop. Testing with TestEventStream<T>.
Part IX: The Saga Pattern
SagaOrchestrator<TContext> with ISagaStep<T> Execute/Compensate. SagaContext state machine: Pending, Running, Completed, Compensating, Compensated, Failed. ISagaStore persistence with SagaInstance. Forward execution, reverse-order compensation on failure. Testing with InMemorySagaStore. Real-world: order fulfillment with three steps and three compensations.
Part X: The Outbox Pattern
The dual-write problem and how to solve it. OutboxMessage with retry tracking. EF Core OutboxInterceptor as a SaveChangesInterceptor that captures domain events from IHasDomainEvents entities. IOutboxProcessor for background publishing. Transactional consistency: domain state and events committed in the same transaction. Testing with InMemoryOutbox.
Part XI: Composition
All nine patterns in one subscription renewal scenario. Guard validates at the API boundary. Option models the subscription lookup. Union models three renewal paths. Mapper transforms between layers. Clock calculates expiration dates. Mediator dispatches through a behavior pipeline. Saga orchestrates payment and provisioning. Outbox guarantees event delivery. Reactive streams feed analytics subscribers. The complete DI registration. The integration test.
How to Read This Series
The foundations-first path (Parts I → II → III → IV): Start here if you want to understand the type-safety building blocks before the behavioral patterns. Option, Union, and Guard are referenced everywhere else.
The architecture path (Parts I → VII → IX → X → XI): Start here if you are an architect evaluating how these patterns compose into an event-driven, CQRS application. Philosophy, Mediator, Saga, Outbox, and the Composition chapter give the full picture.
The pick-and-choose path: Each pattern chapter (Parts II through X) is self-contained. If you only need the Saga pattern, read Part IX. If you only need the Mapper, read Part VI. Part I (Philosophy) provides context that makes every other chapter clearer, but it is not required.
Prerequisites
- .NET 10 / C# 13 (some patterns target
netstandard2.0for broader compatibility) - Familiarity with generic constraints, extension methods, and
async/await - Basic understanding of dependency injection with
IServiceCollection - Optional: familiarity with Roslyn source generators (covered in Part I and Part VI)
- Optional: familiarity with System.Reactive / Rx.NET (covered in Part VIII)
- Optional: familiarity with EF Core interceptors (covered in Part X)
Related Series
This series builds on and references:
- Building a Content Management Framework — the DDD framework that uses these patterns
- Contention over Convention — the philosophy behind attribute-driven source generation
- Entity.Dsl — source-generated entity mapping that uses the Mapper pattern
- From a Big Ball of Mud to DDD — migration patterns that use Guard, Option, and Mediator