Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

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.0 for 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)

This series builds on and references:

⬇ Download