From 0aa020ba844f573d9b14aa0fc13ce7619d8a5858 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:45:12 +0200 Subject: [PATCH 01/19] docs: simplify the main readme --- README.md | 264 +++++++++++++++++------------------------------------- 1 file changed, 83 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 9eeb9f1..202a37f 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ | | | | :-- | :-- | -| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | -| **Quality** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | +| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | +| **Quality** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | | **Security** | [![codeql](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13567/badge)](https://www.bestpractices.dev/projects/13567) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Reefact/first-class-errors/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Reefact/first-class-errors) | -| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | -| **Project** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | +| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | +| **Project** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | --- @@ -17,245 +17,147 @@ ![FirstClassErrors](./doc/images/first-class-errors.png "FirstClassErrors") -FirstClassErrors is a .NET library that treats errors as first-class, documented, and diagnosable concepts — not just strings thrown at runtime. +FirstClassErrors is a .NET library for application errors that need to be understood, diagnosed, documented, and preserved over time. -It helps you: - -* express errors in a consistent and structured way -* attach meaningful diagnostics to each error -* keep error documentation close to the code -* generate human-readable error documentation automatically +Instead of scattering error codes and messages throughout the codebase, you define each meaningful error situation once, in a named factory. The same structured `Error` can then be thrown as an exception, carried in an `Outcome`, logged, and included in a generated error catalog. ## 🚹 The problem -In most systems, errors are: - -* scattered across the codebase -* described by ad-hoc messages -* poorly documented -* hard to troubleshoot -* disconnected from support and operations - -Over time, this leads to: - -* duplicated investigations -* tribal knowledge -* support teams guessing -* developers reinventing error explanations - -## 💡 The idea - -What if: - -> **Every error in your system was explicitly described, structured, and documented — directly in code — and that documentation could be generated automatically?** - -FirstClassErrors introduces: - -* a **rich error model** -* a **structured diagnostic system** -* a **DSL to document errors** -* a **documentation extraction pipeline** - -Errors become: - -> not just failures, -> but **documented knowledge units**. - -## đŸ§± What this library provides - -### 1ïžâƒŁ A richer error model - -The `Error` carries: - -* a stable error code -* a timestamp -* three distinct messages — a mandatory public short summary, an optional public detail, and a mandatory internal diagnostic message -* contextual data -* structured diagnostics - -The exception itself only exposes its `.Error`, and is designed to be: +A production error is rarely useful as only a type and a string: -* logged consistently -* understood by humans -* used by tooling - -#### Three messages, two audiences - -An `Error` carries three messages but deliberately splits them across just **two audiences** — a public one (end users / API clients) and an internal one (logs, support, developers). The separation is enforced by construction, so what reaches a caller can never leak what is meant for developers and support: - -| Message | Mandatory | Audience | Exposure | -| --- | --- | --- | --- | -| `ShortMessage` | Yes | End users / API clients | A short public summary, safe to surface as-is (e.g. the `title` of an RFC 9457 problem detail). | -| `DetailedMessage` | No | End users / API clients | A controlled public detail (e.g. the `detail` of an RFC 9457 problem detail), exposed **only** when the application explicitly chooses to. Must not carry sensitive or internal information. | -| `DiagnosticMessage` | Yes | Logs, support, developers | The internal diagnostic message. May contain technical/operational detail (identifiers, offending values, internal state); it is **never** exposed to external clients by default. | - -The core model is HTTP-agnostic: `DiagnosticMessage` is never used as a default HTTP response body, and `type` and `status` remain the application's concern. When an error is exposed over HTTP, its `Code` is the natural RFC 9457 `type`: surface it as a stable URI such as `urn:problem:{service}:{code}`, where `{code}` is the error code in lowercase kebab-case — for example `urn:problem:temperature-simulator:temperature-below-absolute-zero` or `urn:problem:banking-api:money-transfer-amount-not-positive` — so clients can branch on the problem type. When you turn an error into an exception with `.ToException()`, the resulting `Exception.Message` is the `DiagnosticMessage` — the developer- and log-facing text. - -### 2ïžâƒŁ Structured diagnostics - -Each error can declare **possible causes** and **analysis leads**: - -* What might have caused this error? -* Is it likely input-related, system-related, or both? -* Where should investigation start? - -Diagnostics guide troubleshooting without hardcoding operational processes. - -### 3ïžâƒŁ A DSL to describe errors - -Errors are documented directly in code using a fluent API: - -```csharp -return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); +```text +Invalid operation. ``` -This is not just comments — it is **structured, executable documentation**. - -### 4ïžâƒŁ Documentation extraction - -The library includes a mechanism to scan assemblies and extract all declared error documentation: - -* linked to error factory classes -* linked to factory methods -* enriched with examples -* ready to be rendered - -This enables: +Developers and support still need to discover: -* Markdown or JSON error catalogs (or any custom format via a renderer) -* support-oriented documentation -* living documentation generated from code -* multi-language catalogs (opt-in) — see [Internationalization](doc/Internationalization.en.md) +- which situation actually occurred; +- which rule was violated; +- which facts belong to this occurrence; +- what might have caused it; +- where to start investigating. -## 🔁 Exception or not? You choose. +When that knowledge lives in logs, tickets, comments, and people's memories, it drifts away from the code. -The library supports both: +## 💡 The FirstClassErrors approach -* **throwing errors** (traditional exception flow) -* **transporting errors without throwing** via `Outcome` and `Outcome` - -This lets you treat the same error either way: - -> as a runtime signal you throw -> or as structured data you transport - -depending on the context (domain logic, validation, pipelines, etc.). - -## 📩 Installation - -```bash -dotnet add package FirstClassErrors -``` - -Targets **.NET Standard 2.0**. The Roslyn analyzers are bundled in the package — no separate install. - -## đŸ§© Example - -From the `FirstClassErrors.Usage` project: +A factory gives the error situation a stable identity and keeps its construction in one place: ```csharp -[ProvidesErrorsFor(nameof(Temperature))] -public static class InvalidTemperatureError { +[ProvidesErrorsFor(nameof(Amount))] +public static class InvalidAmountOperationError { - [DocumentedBy(nameof(BelowAbsoluteZeroDocumentation))] - internal static DomainError BelowAbsoluteZero(decimal invalidValue, TemperatureUnit invalidValueUnit) { + [DocumentedBy(nameof(CurrencyMismatchDocumentation))] + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( - Code.TemperatureBelowAbsoluteZero, - diagnosticMessage: $"Failed to instantiate temperature: the value {invalidValue} {invalidValueUnit} is below absolute zero.") + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"), + diagnosticMessage: $"Cannot add {left} and {right} because their currencies differ.") .WithPublicMessage( - shortMessage: "Temperature is invalid.", - detailedMessage: $"The temperature {invalidValue} {invalidValueUnit} is below absolute zero."); + shortMessage: "The amounts use different currencies.", + detailedMessage: "Both amounts must use the same currency."); } - private static ErrorDocumentation BelowAbsoluteZeroDocumentation() { - return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); + private static ErrorDocumentation CurrencyMismatchDocumentation() { + return DescribeError.WithTitle("Amount currency mismatch") + .WithDescription("This error occurs when an operation combines amounts expressed in different currencies.") + .WithRule("A monetary operation must use one common currency.") + .WithExamples(() => CurrencyMismatch( + new Amount(10, Currency.EUR), + new Amount(12, Currency.USD))); } +} +``` - private static class Code { - public static readonly ErrorCode TemperatureBelowAbsoluteZero = ErrorCode.Create("TEMPERATURE_BELOW_ABSOLUTE_ZERO"); - } +The domain code stays focused on intent: + +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } ``` -The factory returns a structured `Error`. When you need to throw it, you turn it into an exception with `.ToException()`: +The factory returns an `Error`, so expected failures can use the same model without throwing: ```csharp -throw InvalidTemperatureError.BelowAbsoluteZero(-1, TemperatureUnit.Kelvin).ToException(); +return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); ``` -Here, the error, its meaning, its rule, its diagnostics, and example messages are all defined together — in code. +From these factories, FirstClassErrors can generate a human-readable catalog for developers, support teams, and operations. -## 🎯 Who is this for? +## 📩 Installation -FirstClassErrors is especially useful if: +```bash +dotnet add package FirstClassErrors +``` -* you build complex business systems -* you care about supportability -* you want consistent error handling -* you want documentation that doesn’t drift from code -* you design with domain-driven thinking +The package targets **.NET Standard 2.0**. Its Roslyn analyzers are bundled automatically; no separate analyzer package is required. -## 🔍 Analyzers +To generate documentation, install the CLI: -FirstClassErrors ships with a set of Roslyn analyzers (rule ids `FCExxx`) **bundled in the NuGet package** — reference the package and they run at build time, no extra install. They catch, before you run anything, the mistakes the runtime or the documentation pipeline would otherwise surface late or silently: duplicate error codes, `[DocumentedBy]` references that don't resolve, documented errors that never reach the catalog, and more. +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -> **Compiler requirement:** the bundled analyzers are compiled against Roslyn 4.8, so they load in any host from **.NET 8 SDK / Visual Studio 2022 17.8** onward. Older SDKs/IDEs cannot load them. +Then follow the [Getting Started guide](doc/GettingStarted.en.md) to create and generate your first documented error. -See the [analyzer rules reference](doc/analyzers/README.md). +## 🎯 When it is a good fit -## 🔐 Supply chain +FirstClassErrors is especially useful for long-lived application or domain code where: -Every released package is built and published by [`release.yml`](.github/workflows/release.yml) with two verifiable guarantees: +- errors represent rules, constraints, or boundary failures; +- several teams or systems depend on stable error codes; +- support and operations investigate production failures; +- documentation must remain aligned with behavior. -- **Signed build provenance** ([SLSA](https://slsa.dev)) — each package is attested at build time, binding its checksum to this repository, the exact commit and the workflow run. The attested bytes are attached to the matching [GitHub Release](https://github.com/Reefact/first-class-errors/releases); download the `.nupkg` from there and verify with: +For prototypes, tiny utilities, or low-level technical code, standard exceptions may be enough. See [When Not to Use FirstClassErrors](doc/WhenNotToUseFirstClassErrors.en.md). - ```bash - gh attestation verify FirstClassErrors..nupkg --repo Reefact/first-class-errors - ``` +## 🔍 Analyzers and supply-chain information - (nuget.org serves a repository-signed copy with a different checksum, so verify **that** one with `dotnet nuget verify` instead.) +The package includes Roslyn rules with stable `FCExxx` identifiers. They detect duplicate or malformed error codes, invalid documentation wiring, missing examples, and common API misuse. See the [analyzer rules reference](doc/analyzers/README.md). -- **Embedded SBOM** — each package carries its SPDX software bill of materials at `_manifest/spdx_2.2/manifest.spdx.json`, inventorying the files it ships and the third-party components it was built from. +Released packages include signed build provenance and an embedded SPDX SBOM. See the release and verification details in the [supply-chain documentation](SECURITY.md). -## 🐛 Feedback & contributing +## 🐛 Feedback and contributing -Found a bug or want to request a feature? Open an issue on the [GitHub issue tracker](https://github.com/Reefact/first-class-errors/issues). Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) to get started. +Found a bug or want to request a feature? Open an issue on the [GitHub issue tracker](https://github.com/Reefact/first-class-errors/issues). Contributions are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). -For **security** vulnerabilities, please follow the private process in [SECURITY.md](SECURITY.md) instead of opening a public issue. +For security vulnerabilities, follow the private process in [SECURITY.md](SECURITY.md). -## 📚 Next steps +## 📚 Documentation -See the full documentation: +### Discover - [Getting Started](doc/GettingStarted.en.md) - [Design Principles](doc/DesignPrinciples.en.md) - [When Not to Use FirstClassErrors](doc/WhenNotToUseFirstClassErrors.en.md) + +### Understand the model + - [Core Concepts](doc/CoreConcepts.en.md) +- [Error Taxonomy and Composition](doc/ErrorTaxonomy.en.md) - [Error Context Guide](doc/ErrorContext.en.md) + +### Write and use errors + - [Writing Errors Guide](doc/WritingErrorsGuide.en.md) - [Usage Patterns](doc/UsagePatterns.en.md) - [Best Practices](doc/BestPractices.en.md) - [Testing Guide](doc/Testing.en.md) + +### Generate and operate the catalog + - [CI/CD and Operational Integration](doc/OperationalIntegration.en.md) - Catalog versioning - - [Overview & workflow](doc/CatalogVersioning.en.md) + - [Overview and workflow](doc/CatalogVersioning.en.md) - [Command reference](doc/CatalogVersioningReference.en.md) - [CI/CD integration](doc/CatalogVersioningCI.en.md) - [Architecture of the Documentation Pipeline](doc/ArchitectureOfTheDocumentationPipeline.en.md) - [Writing a custom renderer](doc/WritingACustomRenderer.en.md) - [Internationalization](doc/Internationalization.en.md) + +### Evaluate and troubleshoot + - [Comparison with error-handling libraries](doc/ComparisonWithOtherLibraries.en.md) - [Analyzer rules (FCExxx)](doc/analyzers/README.md) - [FAQ](doc/FAQ.en.md) From e79b2405a3e25f49ba0be488159b39f31eb010fa Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:45:45 +0200 Subject: [PATCH 02/19] docs: simplify the French readme --- doc/README.fr.md | 268 +++++++++++++++-------------------------------- 1 file changed, 85 insertions(+), 183 deletions(-) diff --git a/doc/README.fr.md b/doc/README.fr.md index 759e5e8..78481dd 100644 --- a/doc/README.fr.md +++ b/doc/README.fr.md @@ -1,15 +1,15 @@ # FirstClassErrors -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](../README.md) | đŸ‡«đŸ‡· Français (ce fichier) | | | | :-- | :-- | -| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | -| **QualitĂ©** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Couverture](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | +| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | +| **QualitĂ©** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Couverture](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | | **SĂ©curitĂ©** | [![codeql](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13567/badge)](https://www.bestpractices.dev/projects/13567) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Reefact/first-class-errors/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Reefact/first-class-errors) | -| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | -| **Projet** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](../LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | +| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | +| **Projet** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](../LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | --- @@ -17,245 +17,147 @@ ![FirstClassErrors](./images/first-class-errors.png "FirstClassErrors") -FirstClassErrors est une bibliothĂšque .NET qui considĂšre les erreurs comme des concepts de premier ordre, documentĂ©s et diagnostiquables — pas seulement comme des chaĂźnes de caractĂšres lancĂ©es Ă  l’exĂ©cution. +FirstClassErrors est une bibliothĂšque .NET destinĂ©e aux erreurs applicatives qui doivent ĂȘtre comprises, diagnostiquĂ©es, documentĂ©es et conservĂ©es dans le temps. -Elle vous aide Ă  : - -* exprimer les erreurs de maniĂšre cohĂ©rente et structurĂ©e -* associer des diagnostics utiles Ă  chaque erreur -* garder la documentation des erreurs proche du code -* gĂ©nĂ©rer automatiquement une documentation humaine des erreurs +Au lieu de disperser les codes et messages d’erreur dans le code, vous dĂ©finissez chaque situation significative une seule fois, dans une factory nommĂ©e. La mĂȘme `Error` structurĂ©e peut ensuite ĂȘtre levĂ©e sous forme d’exception, transportĂ©e dans un `Outcome`, journalisĂ©e et intĂ©grĂ©e Ă  un catalogue d’erreurs gĂ©nĂ©rĂ© automatiquement. ## 🚹 Le problĂšme -Dans la plupart des systĂšmes, les erreurs sont : - -* dispersĂ©es dans le code -* dĂ©crites par des messages ad hoc -* peu ou pas documentĂ©es -* difficiles Ă  analyser -* dĂ©connectĂ©es du support et des opĂ©rations - -Avec le temps, cela entraĂźne : - -* des investigations rĂ©pĂ©tĂ©es -* du savoir implicite (“tribal knowledge”) -* des Ă©quipes support qui devinent -* des dĂ©veloppeurs qui rĂ©expliquent sans cesse les mĂȘmes erreurs - -## 💡 L’idĂ©e - -Et si : - -> **Chaque erreur de votre systĂšme Ă©tait explicitement dĂ©crite, structurĂ©e et documentĂ©e — directement dans le code — et que cette documentation pouvait ĂȘtre gĂ©nĂ©rĂ©e automatiquement ?** - -FirstClassErrors introduit : - -* un **modĂšle d’erreur enrichi** -* un **systĂšme de diagnostics structurĂ©s** -* un **DSL pour documenter les erreurs** -* un **pipeline d’extraction de documentation** - -Les erreurs deviennent : - -> non seulement des Ă©checs, -> mais des **unitĂ©s de connaissance documentĂ©es**. - -## đŸ§± Ce que fournit la bibliothĂšque - -### 1ïžâƒŁ Un modĂšle d’erreur plus riche - -Les erreurs portent : - -* un code d’erreur stable -* un horodatage -* trois messages distincts — un rĂ©sumĂ© public court (obligatoire), un dĂ©tail public optionnel et un message de diagnostic interne (obligatoire) -* des donnĂ©es de contexte -* des diagnostics structurĂ©s - -L’exception, elle, n’expose que son `.Error`, et est conçue pour ĂȘtre : +Une erreur de production est rarement utile lorsqu’elle se rĂ©sume Ă  un type et une chaĂźne de caractĂšres : -* loguĂ©es de maniĂšre cohĂ©rente -* comprises par des humains -* exploitĂ©es par des outils - -#### Trois messages, trois publics - -Une `Error` sĂ©pare dĂ©libĂ©rĂ©ment ce qui peut ĂȘtre montrĂ© Ă  un appelant de ce qui est destinĂ© aux dĂ©veloppeurs et au support : - -| Message | Obligatoire | Public | Exposition | -| --- | --- | --- | --- | -| `ShortMessage` | Oui | Utilisateurs finaux / clients d’API | Un rĂ©sumĂ© public court, exposable tel quel (par ex. le `title` d’un problem detail RFC 9457). | -| `DetailedMessage` | Non | Utilisateurs finaux / clients d’API | Un dĂ©tail public maĂźtrisĂ© (par ex. le `detail` d’un problem detail RFC 9457), exposĂ© **uniquement** si l’application le dĂ©cide explicitement. Ne doit contenir aucune information sensible ou interne. | -| `DiagnosticMessage` | Oui | Logs, support, dĂ©veloppeurs | Le message de diagnostic interne. Il peut contenir des dĂ©tails techniques/opĂ©rationnels (identifiants, valeurs fautives, Ă©tat interne) ; il n’est **jamais** exposĂ© aux clients externes par dĂ©faut. | - -Le cƓur du modĂšle est agnostique vis-Ă -vis d’HTTP : `DiagnosticMessage` n’est jamais utilisĂ© comme corps de rĂ©ponse HTTP par dĂ©faut, et `type` et `status` restent l’affaire de l’application. Lorsqu’une erreur est exposĂ©e en HTTP, son `Code` est le `type` RFC 9457 naturel : exposez-le comme un URI stable tel que `urn:problem:{service}:{code}`, oĂč `{code}` est le code d’erreur en minuscules et en kebab-case — par exemple `urn:problem:temperature-simulator:temperature-below-absolute-zero` ou `urn:problem:banking-api:money-transfer-amount-not-positive` — afin que les clients puissent aiguiller sur le type de problĂšme. Lorsque vous transformez une erreur en exception avec `.ToException()`, le `Exception.Message` obtenu est le `DiagnosticMessage` — le texte destinĂ© aux dĂ©veloppeurs et aux logs. - -### 2ïžâƒŁ Des diagnostics structurĂ©s - -Chaque erreur peut dĂ©clarer des **causes possibles** et des **pistes d’analyse** : - -* Qu’est-ce qui a pu provoquer cette erreur ? -* Est-elle plutĂŽt liĂ©e aux donnĂ©es d’entrĂ©e, au systĂšme, ou aux deux ? -* Par oĂč commencer l’investigation ? - -Les diagnostics orientent l’analyse sans figer les processus opĂ©rationnels. - -### 3ïžâƒŁ Un DSL pour dĂ©crire les erreurs - -Les erreurs sont documentĂ©es directement dans le code via une API fluide : - -```csharp -return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); +```text +OpĂ©ration invalide. ``` -Il ne s’agit pas de simples commentaires — c’est de la **documentation structurĂ©e et exĂ©cutable**. - -### 4ïžâƒŁ Extraction de la documentation - -La bibliothĂšque fournit un mĂ©canisme pour analyser les assemblies et extraire toute la documentation d’erreurs dĂ©clarĂ©e : - -* liĂ©e aux classes de factories d’erreur -* liĂ©e aux mĂ©thodes factory -* enrichie par des exemples -* prĂȘte Ă  ĂȘtre rendue - -Cela permet de gĂ©nĂ©rer : +Les dĂ©veloppeurs et le support doivent encore dĂ©couvrir : -* des catalogues d’erreurs en Markdown ou JSON (ou tout format personnalisĂ© via un renderer) -* de la documentation orientĂ©e support -* une documentation vivante gĂ©nĂ©rĂ©e depuis le code -* des catalogues multilingues (optionnel) — voir [Internationalisation](Internationalisation.fr.md) +- quelle situation s’est rĂ©ellement produite ; +- quelle rĂšgle a Ă©tĂ© violĂ©e ; +- quels faits appartiennent Ă  cette occurrence ; +- ce qui a pu la provoquer ; +- par oĂč commencer l’investigation. -## 🔁 Exception ou pas ? À vous de choisir. +Lorsque cette connaissance est rĂ©partie entre les logs, les tickets, les commentaires et la mĂ©moire des personnes, elle dĂ©rive du code. -La bibliothĂšque supporte Ă  la fois : +## 💡 L’approche FirstClassErrors -* **les erreurs levĂ©es** (flux classique par exceptions) -* **les erreurs transportĂ©es sans ĂȘtre levĂ©es** via `Outcome` et `Outcome` - -Cela vous permet de traiter la mĂȘme erreur, au choix : - -> comme un signal d’exĂ©cution que vous levez -> ou comme une donnĂ©e structurĂ©e que vous transportez - -selon le contexte (domaine, validation, pipelines, etc.). - -## 📩 Installation - -```bash -dotnet add package FirstClassErrors -``` - -Cible **.NET Standard 2.0**. Les analyseurs Roslyn sont intĂ©grĂ©s au package — aucune installation sĂ©parĂ©e. - -## đŸ§© Exemple - -Extrait du projet `FirstClassErrors.Usage` : +Une factory donne une identitĂ© stable Ă  la situation d’erreur et centralise sa construction : ```csharp -[ProvidesErrorsFor(nameof(Temperature))] -public static class InvalidTemperatureError { +[ProvidesErrorsFor(nameof(Amount))] +public static class InvalidAmountOperationError { - [DocumentedBy(nameof(BelowAbsoluteZeroDocumentation))] - internal static DomainError BelowAbsoluteZero(decimal invalidValue, TemperatureUnit invalidValueUnit) { + [DocumentedBy(nameof(CurrencyMismatchDocumentation))] + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( - Code.TemperatureBelowAbsoluteZero, - diagnosticMessage: $"Failed to instantiate temperature: the value {invalidValue} {invalidValueUnit} is below absolute zero.") + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"), + diagnosticMessage: $"Impossible d’additionner {left} et {right} car leurs devises diffĂšrent.") .WithPublicMessage( - shortMessage: "Temperature is below absolute zero.", - detailedMessage: "The provided temperature is below absolute zero, which is not physically valid."); + shortMessage: "Les montants utilisent des devises diffĂ©rentes.", + detailedMessage: "Les deux montants doivent utiliser la mĂȘme devise."); } - private static ErrorDocumentation BelowAbsoluteZeroDocumentation() { - return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); + private static ErrorDocumentation CurrencyMismatchDocumentation() { + return DescribeError.WithTitle("IncohĂ©rence de devise") + .WithDescription("Cette erreur survient lorsqu’une opĂ©ration combine des montants exprimĂ©s dans des devises diffĂ©rentes.") + .WithRule("Une opĂ©ration monĂ©taire doit utiliser une devise commune.") + .WithExamples(() => CurrencyMismatch( + new Amount(10, Currency.EUR), + new Amount(12, Currency.USD))); } +} +``` - private static class Code { - public static readonly ErrorCode TemperatureBelowAbsoluteZero = ErrorCode.Create("TEMPERATURE_BELOW_ABSOLUTE_ZERO"); - } +Le code mĂ©tier reste centrĂ© sur l’intention : + +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } ``` -La factory retourne une `Error` structurĂ©e. Lorsque vous devez la lever, vous la transformez en exception avec `.ToException()` : +La factory retourne une `Error`, donc un Ă©chec attendu peut employer le mĂȘme modĂšle sans lever d’exception : ```csharp -throw InvalidTemperatureError.BelowAbsoluteZero(-1, TemperatureUnit.Kelvin).ToException(); +return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); ``` -Ici, l’erreur, sa signification, sa rĂšgle, ses diagnostics et des exemples de messages sont dĂ©finis ensemble — dans le code. +À partir de ces factories, FirstClassErrors peut gĂ©nĂ©rer un catalogue lisible par les dĂ©veloppeurs, le support et l’exploitation. -## 🎯 Pour qui ? +## 📩 Installation -FirstClassErrors est particuliĂšrement utile si : +```bash +dotnet add package FirstClassErrors +``` -* vous dĂ©veloppez des systĂšmes mĂ©tiers complexes -* vous vous souciez de la supportabilitĂ© -* vous voulez une gestion d’erreurs cohĂ©rente -* vous souhaitez une documentation qui ne dĂ©rive pas du code -* vous concevez avec une approche orientĂ©e domaine +Le package cible **.NET Standard 2.0**. Les analyseurs Roslyn sont inclus automatiquement ; aucun package d’analyse supplĂ©mentaire n’est nĂ©cessaire. -## 🔍 Analyseurs +Pour gĂ©nĂ©rer la documentation, installez le CLI : -FirstClassErrors est livrĂ© avec un ensemble d’analyseurs Roslyn (identifiants de rĂšgle `FCExxx`) **inclus dans le package NuGet** — rĂ©fĂ©rencez le package et ils s’exĂ©cutent au build, sans installation supplĂ©mentaire. Ils dĂ©tectent, avant toute exĂ©cution, les erreurs que le runtime ou le pipeline de documentation ne feraient sinon apparaĂźtre que tard, voire silencieusement : codes d’erreur dupliquĂ©s, rĂ©fĂ©rences `[DocumentedBy]` qui ne rĂ©solvent pas, erreurs documentĂ©es qui n’atteignent jamais le catalogue, et plus encore. +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -> **PrĂ©requis compilateur :** les analyseurs inclus sont compilĂ©s contre Roslyn 4.8 ; ils se chargent donc dans tout hĂŽte Ă  partir du **SDK .NET 8 / Visual Studio 2022 17.8**. Les SDK/IDE plus anciens ne peuvent pas les charger. +Suivez ensuite le guide [Premiers pas](GettingStarted.fr.md) pour crĂ©er et gĂ©nĂ©rer votre premiĂšre erreur documentĂ©e. -Voir la [rĂ©fĂ©rence des rĂšgles d’analyse](analyzers/README.fr.md). +## 🎯 Quand la bibliothĂšque est adaptĂ©e -## 🔐 ChaĂźne d’approvisionnement +FirstClassErrors est particuliĂšrement utile dans du code applicatif ou mĂ©tier durable lorsque : -Chaque package publiĂ© est construit et poussĂ© par [`release.yml`](../.github/workflows/release.yml) avec deux garanties vĂ©rifiables : +- les erreurs reprĂ©sentent des rĂšgles, des contraintes ou des dĂ©faillances de frontiĂšre ; +- plusieurs Ă©quipes ou systĂšmes dĂ©pendent de codes d’erreur stables ; +- le support et l’exploitation investiguent des incidents de production ; +- la documentation doit rester alignĂ©e avec le comportement. -- **Provenance de build signĂ©e** ([SLSA](https://slsa.dev)) — chaque package est attestĂ© au moment du build, liant son empreinte Ă  ce dĂ©pĂŽt, au commit exact et Ă  l’exĂ©cution du workflow. Les octets attestĂ©s sont attachĂ©s Ă  la [GitHub Release](https://github.com/Reefact/first-class-errors/releases) correspondante ; tĂ©lĂ©chargez-y le `.nupkg` et vĂ©rifiez avec : +Pour un prototype, un petit utilitaire ou du code technique bas niveau, les exceptions standards peuvent suffire. Consultez [Quand ne pas utiliser FirstClassErrors](WhenNotToUseFirstClassErrors.fr.md). - ```bash - gh attestation verify FirstClassErrors..nupkg --repo Reefact/first-class-errors - ``` +## 🔍 Analyseurs et chaĂźne d’approvisionnement - (nuget.org sert une copie signĂ©e par le dĂ©pĂŽt, d’empreinte diffĂ©rente : vĂ©rifiez **celle-ci** plutĂŽt avec `dotnet nuget verify`.) +Le package contient des rĂšgles Roslyn aux identifiants stables `FCExxx`. Elles dĂ©tectent notamment les codes dupliquĂ©s ou mal formĂ©s, les liaisons de documentation invalides, l’absence d’exemples et certains mauvais usages de l’API. Consultez la [rĂ©fĂ©rence des analyseurs](analyzers/README.fr.md). -- **SBOM embarquĂ©** — chaque package contient son inventaire logiciel SPDX (*software bill of materials*) Ă  `_manifest/spdx_2.2/manifest.spdx.json`, recensant les fichiers livrĂ©s et les composants tiers utilisĂ©s pour le construire. +Les packages publiĂ©s contiennent une provenance de build signĂ©e et un SBOM SPDX embarquĂ©. Les informations de vĂ©rification sont disponibles dans [SECURITY.fr.md](SECURITY.fr.md). -## 🐛 Retours & contributions +## 🐛 Retours et contributions -Vous avez trouvĂ© un bug ou souhaitez proposer une fonctionnalitĂ© ? Ouvrez une issue sur le [gestionnaire d’issues GitHub](https://github.com/Reefact/first-class-errors/issues). Les contributions sont les bienvenues — voir [CONTRIBUTING.fr.md](./CONTRIBUTING.fr.md) pour commencer. +Vous avez trouvĂ© un bug ou souhaitez proposer une fonctionnalitĂ© ? Ouvrez une issue sur le [gestionnaire d’issues GitHub](https://github.com/Reefact/first-class-errors/issues). Les contributions sont les bienvenues ; consultez [CONTRIBUTING.fr.md](CONTRIBUTING.fr.md). -Pour les vulnĂ©rabilitĂ©s de **sĂ©curitĂ©**, merci de suivre le processus privĂ© dĂ©crit dans [SECURITY.fr.md](./SECURITY.fr.md) plutĂŽt que d’ouvrir une issue publique. +Pour les vulnĂ©rabilitĂ©s de sĂ©curitĂ©, suivez le processus privĂ© dĂ©crit dans [SECURITY.fr.md](SECURITY.fr.md). -## 📚 Étapes suivantes +## 📚 Documentation -Consultez la documentation complĂšte : +### DĂ©couvrir - [Premiers pas](GettingStarted.fr.md) - [Principes de conception](DesignPrinciples.fr.md) - [Quand ne pas utiliser FirstClassErrors](WhenNotToUseFirstClassErrors.fr.md) + +### Comprendre le modĂšle + - [Concepts fondamentaux](CoreConcepts.fr.md) +- [Taxonomie et composition des erreurs](ErrorTaxonomy.fr.md) - [Guide du contexte d’erreur](ErrorContext.fr.md) + +### Écrire et utiliser des erreurs + - [Guide d’écriture des erreurs](WritingErrorsGuide.fr.md) - [Cas d’usage](UsagePatterns.fr.md) - [Bonnes pratiques](BestPractices.fr.md) - [Guide des tests](Testing.fr.md) + +### GĂ©nĂ©rer et exploiter le catalogue + - [IntĂ©gration CI/CD et exploitation](OperationalIntegration.fr.md) - Versionnage du catalogue - - [Vue d'ensemble & workflow](CatalogVersioning.fr.md) + - [Vue d’ensemble et workflow](CatalogVersioning.fr.md) - [RĂ©fĂ©rence des commandes](CatalogVersioningReference.fr.md) - [IntĂ©gration CI/CD](CatalogVersioningCI.fr.md) - [Architecture du pipeline de documentation](ArchitectureOfTheDocumentationPipeline.fr.md) - [Écrire son propre renderer](WritingACustomRenderer.fr.md) - [Internationalisation](Internationalisation.fr.md) -- [Comparaison avec les librairies de gestion d’erreurs](ComparisonWithOtherLibraries.fr.md) + +### Évaluer et rĂ©soudre les problĂšmes + +- [Comparaison avec les bibliothĂšques de gestion d’erreurs](ComparisonWithOtherLibraries.fr.md) - [RĂšgles d’analyse (FCExxx)](analyzers/README.fr.md) - [FAQ](FAQ.fr.md) From 36213ea09361c4773e983e14cfe22928ef2bb3b6 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:46:13 +0200 Subject: [PATCH 03/19] docs: make getting started end to end --- doc/GettingStarted.en.md | 210 ++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 113 deletions(-) diff --git a/doc/GettingStarted.en.md b/doc/GettingStarted.en.md index 053112e..fdb8e9d 100644 --- a/doc/GettingStarted.en.md +++ b/doc/GettingStarted.en.md @@ -3,192 +3,176 @@ 🌍 **Languages:** 🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./GettingStarted.fr.md) -FirstClassErrors helps you treat errors as **structured, diagnosable knowledge** instead of simple exception messages. +This guide takes you from an empty project to your first generated error catalog. -In a few minutes, you will see how to: +You will: -* define an error (with a factory) -* use error factories (required for living documentation) -* document the error in a structured way -* attach diagnostics -* optionally use the error without throwing +1. install the library and CLI; +2. opt a project into documentation generation; +3. define one documented error; +4. use it in code; +5. generate the catalog. -## 1ïžâƒŁ Define an error (with a factory) +## 1. Install the package and CLI -To benefit from **living documentation**, errors are not created directly with `new`. Instead, they are created through **static factory methods** inside the error class. +In the application project: -This pattern is essential because: +```bash +dotnet add package FirstClassErrors +``` + +Install the documentation CLI once on your machine: + +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -* each factory method represents a **specific error situation** -* it is the anchor point for the documentation DSL -* the documentation generator links factories to documentation +## 2. Opt the project into documentation generation -Note: +Add this property directly to the project file that contains your errors: -*Using factory methods to create errors is a well-established .NET pattern for centralizing and standardizing error creation. FirstClassErrors builds on this idea and makes error factories the anchor point for structured, living error documentation. Beyond documentation, factories significantly improve code readability: they keep error construction (error codes, messages, formatting, and wording) out of the “happy path,” allowing domain logic to remain focused on business rules rather than technical details. A call such as `throw InvalidAmountOperationError.CurrencyMismatch(a1, a2).ToException();` expresses intent far more clearly than inlined error construction. This approach aligns with clean code principles by separating concerns, reducing duplication, and giving each error situation a named, explicit representation in the codebase — while also providing a single, consistent place to attach diagnostics and documentation.* +```xml + + true + +``` + +The marker must be present in the `.csproj` itself. Projects without it are skipped when the CLI scans a solution. -Example: +## 3. Define one error situation + +Create a static factory class. Each factory method represents one precise situation the system recognizes. ```csharp +using FirstClassErrors; + [ProvidesErrorsFor(nameof(Amount))] public static class InvalidAmountOperationError { [DocumentedBy(nameof(CurrencyMismatchDocumentation))] - internal static DomainError CurrencyMismatch(Amount amount1, Amount amount2) { + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( Code.CurrencyMismatch, - diagnosticMessage: $"Failed to perform the monetary operation because the involved amounts are expressed in different currencies: {amount1} and {amount2}.") + diagnosticMessage: $"Cannot add {left} and {right} because their currencies differ.") .WithPublicMessage( - shortMessage: "Currency mismatch", - detailedMessage: "The amounts involved in the operation are expressed in different currencies."); + shortMessage: "The amounts use different currencies.", + detailedMessage: "Both amounts must use the same currency."); } private static class Code { - public static readonly ErrorCode CurrencyMismatch = ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); + public static readonly ErrorCode CurrencyMismatch = + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); } - } ``` -Here: - -* The **error type** represents a category of domain errors. -* The **factory method** represents a precise error case. -* The **error code** is stable and machine-readable. -* The factory method is what will be documented. +The three important parts are: -The error is built in two stages: `DomainError.Create(...)` captures the mandatory internal information (the error `code` and the internal `diagnosticMessage`), and `.WithPublicMessage(shortMessage, detailedMessage)` supplies the public-facing messages and produces the final error. The mandatory `shortMessage` is a safe public summary, the optional `detailedMessage` is a controlled public detail, and the `diagnosticMessage` is for logs, support and developers — never exposed to clients by default. There is no `.Build()`, and the public constructors are internal, so an error can never be left without its public message. +- the factory name expresses the situation in code; +- the error code is its stable, machine-readable identity; +- the diagnostic message is internal, while the public messages are safe for callers. -You never `new` the error or the exception yourself: the error is created through the staged builder inside the factory, and when you need to throw, you call `error.ToException()` (see section 4). +## 4. Add the structured documentation -## 2ïžâƒŁ Link the factory to structured documentation - -Each factory method is linked to documentation using `[DocumentedBy]`. +The `[DocumentedBy]` attribute links the factory to a documentation method in the same class: ```csharp private static ErrorDocumentation CurrencyMismatchDocumentation() { return DescribeError.WithTitle("Amount currency mismatch") - .WithDescription("This error occurs when trying to use multiple amounts together in an operation while they are expressed in different currencies.") - .WithRule("All monetary operations must involve amounts expressed in the same currency.") + .WithDescription( + "This error occurs when an operation combines amounts expressed in different currencies.") + .WithRule( + "A monetary operation must use one common currency.") .WithDiagnostic( - "Amounts were used in a monetary operation without having been converted to the same currency.", + "The amounts reached the operation without being converted to one currency.", ErrorOrigin.Internal, - "Verify whether all amounts involved in the operation were converted to a common currency before being used together." - ) + "Verify where the amounts should have been converted before this operation.") .AndDiagnostic( - "Amounts expected to be expressed in the same currency were provided with different currencies.", - ErrorOrigin.InternalOrExternal, - "Check the currencies associated with each amount and confirm whether a common currency was expected for this operation." - ) - .WithExamples(() => CurrencyMismatch(new Amount(127.33m, Currency.EUR), new Amount(57689.00m, Currency.USD))); + "The caller provided amounts in currencies that cannot be combined directly.", + ErrorOrigin.External, + "Check the currencies supplied by the caller.") + .WithExamples(() => CurrencyMismatch( + new Amount(10m, Currency.EUR), + new Amount(12m, Currency.USD))); } ``` -Each diagnostic declares an **origin** via `ErrorOrigin`, whose values are `Internal`, `External`, and `InternalOrExternal` — indicating whether the cause lies inside the system, outside it (input), or could be either. - -This documentation: - -* explains what the error means -* states the violated rule -* provides diagnostic hypotheses -* gives realistic example messages - -This is structured knowledge, not a comment. - -## 3ïžâƒŁ Add structured error context (`ErrorContext`) - -When information helps diagnose **a specific occurrence**, attach it as context. - -```csharp -return PrimaryPortError.Create( - Code.DateOutOfStatementPeriod, - diagnosticMessage: $"Transaction dated {transactionDate} is outside the statement period [{periodStart};{periodEnd}].", - transience: Transience.NonTransient, - configureContext: ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) - .WithPublicMessage( - shortMessage: "Transaction date is outside the statement period.", - detailedMessage: "The transaction date falls outside the allowed statement period."); -``` - -The context lives on the `Error`; when an exception is later produced with `error.ToException()`, it is reached through `exception.Error.Context`. - -Best practices: +This is structured knowledge rather than a comment: the generator can extract the title, explanation, rule, diagnostics, and real example produced by the factory. -* use named, stable keys (`ErrorContextKey`) -* add context at factory level -* avoid sensitive or oversized data +## 5. Use the error -## 4ïžâƒŁ Use the exception in domain code +When the failure is exceptional, turn the error into its paired exception: ```csharp public Amount Add(Amount other) { - if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } + if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); + } return new Amount(Value + other.Value, Currency); } ``` -Domain logic remains clean and expressive. - -## 5ïžâƒŁ Or use it without throwing (`Outcome`) +The business code names the situation without repeating codes or messages. -For validation or batch scenarios: +When failure is an expected part of the flow, carry the same `Error` without throwing: ```csharp -public static Outcome TryAdd(Amount a1, Amount a2) { - if (a1.Currency != a2.Currency) { - return Outcome.Failure(InvalidAmountOperationError.CurrencyMismatch(a1, a2)); +public static Outcome TryAdd(Amount left, Amount right) { + if (left.Currency != right.Currency) { + return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); } - return Outcome.Success(new Amount(a1.Value + a2.Value, a1.Currency)); + return Outcome.Success( + new Amount(left.Value + right.Value, left.Currency)); } ``` -Note: `Failure(...)` takes an **`Error`** — the factory returns one directly, so no exception is involved. +These two usages do not define two different errors. They transport the same error situation in different ways. -You can inspect: +## 6. Generate the catalog -```csharp -if (result.IsFailure) { - Log(result.Error); -} -``` +Build the solution, then generate a Markdown catalog: -Or escalate: +```bash +dotnet build MyApp.sln -c Release -```csharp -var amount = result.GetResultOrThrow(); +fce generate \ + --solution MyApp.sln \ + --no-build \ + --format markdown \ + --service-name my-api \ + --output artifacts/errors.md ``` -## 6ïžâƒŁ Generate documentation +The generated document contains an entry for `AMOUNT_CURRENCY_MISMATCH`, including its description, rule, diagnostic hypotheses, and the messages produced by the example factory. -Because factories are linked to structured documentation: +A shortened result looks like this: -* errors can be extracted from assemblies -* documentation can be generated automatically -* support and developers share the same source of truth +```markdown +## Amount currency mismatch -## ✅ What you gain +**Code:** `AMOUNT_CURRENCY_MISMATCH` -With FirstClassErrors: +This error occurs when an operation combines amounts expressed in different currencies. -* errors are consistent -* documentation is close to the code -* diagnostics guide troubleshooting -* knowledge does not drift - -You move from: +**Rule:** A monetary operation must use one common currency. +``` -> “An exception happened” +Commit or publish this generated catalog according to your delivery workflow. For automatic generation in CI, see [CI/CD and Operational Integration](OperationalIntegration.en.md). -to +## Optional next steps -> “This specific, documented error occurred, here is what it means and where to look.” +- Add occurrence-specific facts with [Error Context](ErrorContext.en.md). +- Understand the available error categories in [Error Taxonomy and Composition](ErrorTaxonomy.en.md). +- Learn when to throw or return an `Outcome` in [Usage Patterns](UsagePatterns.en.md). +- Protect stable codes and context keys with [Catalog Versioning](CatalogVersioning.en.md). --- ---- \ No newline at end of file +--- From eb20503761e8c494e3f7a24900348d349331cdb9 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:46:38 +0200 Subject: [PATCH 04/19] docs: make the French getting started guide end to end --- doc/GettingStarted.fr.md | 218 ++++++++++++++++++--------------------- 1 file changed, 101 insertions(+), 117 deletions(-) diff --git a/doc/GettingStarted.fr.md b/doc/GettingStarted.fr.md index 3e6a609..929e5a9 100644 --- a/doc/GettingStarted.fr.md +++ b/doc/GettingStarted.fr.md @@ -1,194 +1,178 @@ -# Premier pas +# Premiers pas -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](./GettingStarted.en.md) | đŸ‡«đŸ‡· Français (ce fichier) -FirstClassErrors vous aide Ă  considĂ©rer les erreurs comme une **connaissance structurĂ©e et diagnostiquable**, plutĂŽt que comme de simples messages d’exception. +Ce guide vous mĂšne d’un projet vide Ă  votre premier catalogue d’erreurs gĂ©nĂ©rĂ©. -En quelques minutes, vous allez voir comment : +Vous allez : -* dĂ©finir une erreur (avec une factory) -* utiliser des factories d’erreur (indispensables pour la documentation vivante) -* documenter l’erreur de maniĂšre structurĂ©e -* attacher des diagnostics -* utiliser Ă©ventuellement l’erreur sans la lever +1. installer la bibliothĂšque et le CLI ; +2. activer la gĂ©nĂ©ration pour un projet ; +3. dĂ©finir une erreur documentĂ©e ; +4. l’utiliser dans le code ; +5. gĂ©nĂ©rer le catalogue. -## 1ïžâƒŁ DĂ©finir une erreur (avec une factory) +## 1. Installer le package et le CLI -Pour bĂ©nĂ©ficier de la **documentation vivante**, les erreurs ne sont pas créées directement avec `new`. Elles sont créées via des **mĂ©thodes factory statiques** dans la classe d’erreur. +Dans le projet applicatif : -Ce pattern est essentiel car : +```bash +dotnet add package FirstClassErrors +``` + +Installez une fois le CLI de documentation sur votre machine : + +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -* chaque mĂ©thode factory reprĂ©sente une **situation d’erreur spĂ©cifique** -* c’est le point d’ancrage du DSL de documentation -* le gĂ©nĂ©rateur de documentation relie les factories Ă  la documentation +## 2. Activer la gĂ©nĂ©ration pour le projet -Remarque : +Ajoutez cette propriĂ©tĂ© directement dans le fichier du projet qui contient vos erreurs : -*L’utilisation de mĂ©thodes factory pour crĂ©er des erreurs est un pattern .NET bien Ă©tabli pour centraliser et standardiser la crĂ©ation d’erreurs. FirstClassErrors s’appuie sur cette idĂ©e et fait des factories le point d’ancrage de la documentation d’erreurs structurĂ©e et vivante. Au-delĂ  de la documentation, les factories amĂ©liorent fortement la lisibilitĂ© du code : elles sortent la construction de l’erreur (codes, messages, formatage, formulation) du “happy path”, ce qui permet Ă  la logique mĂ©tier de rester centrĂ©e sur les rĂšgles mĂ©tier plutĂŽt que sur des dĂ©tails techniques. Un appel comme `throw InvalidAmountOperationError.CurrencyMismatch(a1, a2).ToException();` exprime l’intention bien plus clairement qu’une construction d’erreur inline. Cette approche s’aligne avec les principes du clean code en sĂ©parant les responsabilitĂ©s, en rĂ©duisant la duplication et en donnant Ă  chaque situation d’erreur une reprĂ©sentation explicite et nommĂ©e dans le code — tout en fournissant un point unique et cohĂ©rent pour attacher diagnostics et documentation.* +```xml + + true + +``` + +Le marqueur doit ĂȘtre prĂ©sent dans le `.csproj` lui-mĂȘme. Les projets qui ne le dĂ©clarent pas sont ignorĂ©s lorsque le CLI analyse une solution. -Exemple : +## 3. DĂ©finir une situation d’erreur + +CrĂ©ez une classe factory statique. Chaque mĂ©thode factory reprĂ©sente une situation prĂ©cise reconnue par le systĂšme. ```csharp +using FirstClassErrors; + [ProvidesErrorsFor(nameof(Amount))] public static class InvalidAmountOperationError { [DocumentedBy(nameof(CurrencyMismatchDocumentation))] - internal static DomainError CurrencyMismatch(Amount amount1, Amount amount2) { + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( Code.CurrencyMismatch, - diagnosticMessage: $"Impossible d’effectuer l’opĂ©ration monĂ©taire car les montants impliquĂ©s sont exprimĂ©s dans des devises diffĂ©rentes : {amount1} et {amount2}.") + diagnosticMessage: $"Impossible d’additionner {left} et {right} car leurs devises diffĂšrent.") .WithPublicMessage( - shortMessage: "Devise diffĂ©rente", - detailedMessage: "Les montants impliquĂ©s dans l’opĂ©ration sont exprimĂ©s dans des devises diffĂ©rentes."); + shortMessage: "Les montants utilisent des devises diffĂ©rentes.", + detailedMessage: "Les deux montants doivent utiliser la mĂȘme devise."); } private static class Code { - public static readonly ErrorCode CurrencyMismatch = ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); + public static readonly ErrorCode CurrencyMismatch = + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); } - } -```` - -Ici : - -* Le **type d’erreur** reprĂ©sente une catĂ©gorie d’erreurs mĂ©tier. -* La **mĂ©thode factory** reprĂ©sente un cas d’erreur prĂ©cis. -* Le **code d’erreur** est stable et lisible par machine. -* C’est la mĂ©thode factory qui sera documentĂ©e. +``` -L’erreur est construite en deux Ă©tapes : `DomainError.Create(...)` capture l’information interne obligatoire (le `code` d’erreur et le `diagnosticMessage` interne), et `.WithPublicMessage(shortMessage, detailedMessage)` fournit les messages publics et produit l’erreur finale. Le `shortMessage` obligatoire est un rĂ©sumĂ© public sĂ»r, le `detailedMessage` optionnel est un dĂ©tail public maĂźtrisĂ©, et le `diagnosticMessage` est destinĂ© aux logs, au support et aux dĂ©veloppeurs — jamais exposĂ© aux clients par dĂ©faut. Il n’y a pas de `.Build()`, et les constructeurs publics sont internes, de sorte qu’une erreur ne peut jamais rester sans son message public. +Trois Ă©lĂ©ments sont essentiels : -Vous ne faites jamais `new` sur l’erreur ni sur l’exception vous-mĂȘme : l’erreur est créée via le builder Ă©tagĂ© dans la factory, et pour lever, vous appelez `error.ToException()` (voir section 4). +- le nom de la factory exprime la situation dans le code ; +- le code d’erreur constitue son identitĂ© stable et lisible par machine ; +- le message de diagnostic est interne, tandis que les messages publics sont sĂ»rs pour les appelants. -## 2ïžâƒŁ Lier la factory Ă  une documentation structurĂ©e +## 4. Ajouter la documentation structurĂ©e -Chaque mĂ©thode factory est liĂ©e Ă  sa documentation via `[DocumentedBy]`. +L’attribut `[DocumentedBy]` relie la factory Ă  une mĂ©thode de documentation situĂ©e dans la mĂȘme classe : ```csharp private static ErrorDocumentation CurrencyMismatchDocumentation() { - return DescribeError.WithTitle("IncohĂ©rence de devise des montants") - .WithDescription("Cette erreur survient lorsqu’on tente d’utiliser plusieurs montants dans une opĂ©ration alors qu’ils sont exprimĂ©s dans des devises diffĂ©rentes.") - .WithRule("Toutes les opĂ©rations monĂ©taires doivent impliquer des montants exprimĂ©s dans la mĂȘme devise.") + return DescribeError.WithTitle("IncohĂ©rence de devise") + .WithDescription( + "Cette erreur survient lorsqu’une opĂ©ration combine des montants exprimĂ©s dans des devises diffĂ©rentes.") + .WithRule( + "Une opĂ©ration monĂ©taire doit utiliser une devise commune.") .WithDiagnostic( - "Des montants ont Ă©tĂ© utilisĂ©s dans une opĂ©ration monĂ©taire sans avoir Ă©tĂ© convertis dans une devise commune.", + "Les montants ont atteint l’opĂ©ration sans avoir Ă©tĂ© convertis dans une devise commune.", ErrorOrigin.Internal, - "VĂ©rifiez si tous les montants impliquĂ©s ont Ă©tĂ© convertis dans une devise commune avant d’ĂȘtre utilisĂ©s ensemble." - ) + "VĂ©rifiez Ă  quel endroit les montants auraient dĂ» ĂȘtre convertis avant cette opĂ©ration.") .AndDiagnostic( - "Des montants censĂ©s ĂȘtre exprimĂ©s dans la mĂȘme devise ont Ă©tĂ© fournis avec des devises diffĂ©rentes.", - ErrorOrigin.InternalOrExternal, - "VĂ©rifiez les devises associĂ©es Ă  chaque montant et confirmez si une devise commune Ă©tait attendue pour cette opĂ©ration." - ) - .WithExamples(() => CurrencyMismatch(new Amount(127.33m, Currency.EUR), new Amount(57689.00m, Currency.USD))); + "L’appelant a fourni des montants dans des devises qui ne peuvent pas ĂȘtre combinĂ©es directement.", + ErrorOrigin.External, + "VĂ©rifiez les devises fournies par l’appelant.") + .WithExamples(() => CurrencyMismatch( + new Amount(10m, Currency.EUR), + new Amount(12m, Currency.USD))); } ``` -Chaque diagnostic dĂ©clare une **origine** via `ErrorOrigin`, dont les valeurs sont `Internal`, `External` et `InternalOrExternal` — indiquant si la cause se situe Ă  l’intĂ©rieur du systĂšme, Ă  l’extĂ©rieur (entrĂ©e) ou peut ĂȘtre l’une ou l’autre. +Il s’agit de connaissance structurĂ©e, pas d’un commentaire : le gĂ©nĂ©rateur peut extraire le titre, l’explication, la rĂšgle, les hypothĂšses de diagnostic et l’exemple rĂ©ellement produit par la factory. -Cette documentation : +## 5. Utiliser l’erreur -* explique la signification de l’erreur -* Ă©nonce la rĂšgle violĂ©e -* fournit des hypothĂšses de diagnostic -* donne des exemples rĂ©alistes de messages - -Il s’agit de connaissance structurĂ©e, pas d’un commentaire. - -## 3ïžâƒŁ Ajouter un contexte d’erreur structurĂ© (`ErrorContext`) - -Quand une information est utile pour diagnostiquer **une occurrence prĂ©cise**, ajoutez-la dans le contexte. - -```csharp -return PrimaryPortError.Create( - Code.DateOutOfStatementPeriod, - diagnosticMessage: $"Transaction datĂ©e du {transactionDate} hors pĂ©riode [{periodStart};{periodEnd}].", - transience: Transience.NonTransient, - configureContext: ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) - .WithPublicMessage( - shortMessage: "Date de transaction hors pĂ©riode.", - detailedMessage: "La date de transaction se situe hors de la pĂ©riode du relevĂ© autorisĂ©e."); -``` - -Le contexte est portĂ© par l’`Error` ; lorsqu’une exception est ensuite produite avec `error.ToException()`, on y accĂšde via `exception.Error.Context`. - -Bonnes pratiques : - -* utilisez des clĂ©s nommĂ©es et stables (`ErrorContextKey`) -* ajoutez le contexte au niveau des factories -* Ă©vitez les donnĂ©es sensibles ou trop volumineuses - -## 4ïžâƒŁ Utiliser l’exception dans le code mĂ©tier +Lorsque l’échec est exceptionnel, transformez l’erreur en son exception associĂ©e : ```csharp public Amount Add(Amount other) { - if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } + if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); + } return new Amount(Value + other.Value, Currency); } ``` -La logique mĂ©tier reste propre et expressive. - -## 5ïžâƒŁ Ou l’utiliser sans lever d’exception (`Outcome`) +Le code mĂ©tier nomme la situation sans rĂ©pĂ©ter les codes ni les messages. -Pour les scĂ©narios de validation ou de traitement par lots : +Lorsque l’échec fait normalement partie du flux, transportez la mĂȘme `Error` sans la lever : ```csharp -public static Outcome TryAdd(Amount a1, Amount a2) { - if (a1.Currency != a2.Currency) { - return Outcome.Failure(InvalidAmountOperationError.CurrencyMismatch(a1, a2)); +public static Outcome TryAdd(Amount left, Amount right) { + if (left.Currency != right.Currency) { + return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); } - return Outcome.Success(new Amount(a1.Value + a2.Value, a1.Currency)); + return Outcome.Success( + new Amount(left.Value + right.Value, left.Currency)); } ``` -Remarque : `Failure(...)` prend une **`Error`** — la factory en renvoie une directement, donc aucune exception n’est impliquĂ©e. +Ces deux usages ne dĂ©finissent pas deux erreurs diffĂ©rentes. Ils transportent la mĂȘme situation d’erreur de deux maniĂšres diffĂ©rentes. -Vous pouvez inspecter : +## 6. GĂ©nĂ©rer le catalogue -```csharp -if (result.IsFailure) { - Log(result.Error); -} -``` +Construisez la solution, puis gĂ©nĂ©rez un catalogue Markdown : -Ou escalader : +```bash +dotnet build MyApp.sln -c Release -```csharp -var amount = result.GetResultOrThrow(); +fce generate \ + --solution MyApp.sln \ + --no-build \ + --format markdown \ + --service-name my-api \ + --output artifacts/errors.md ``` -## 6ïžâƒŁ GĂ©nĂ©rer la documentation +Le document gĂ©nĂ©rĂ© contient une entrĂ©e pour `AMOUNT_CURRENCY_MISMATCH`, avec sa description, sa rĂšgle, ses hypothĂšses de diagnostic et les messages produits par l’exemple. -Comme les factories sont liĂ©es Ă  une documentation structurĂ©e : +Un rĂ©sultat abrĂ©gĂ© ressemble Ă  ceci : -* les erreurs peuvent ĂȘtre extraites des assemblies -* la documentation peut ĂȘtre gĂ©nĂ©rĂ©e automatiquement -* le support et les dĂ©veloppeurs partagent la mĂȘme source de vĂ©ritĂ© +```markdown +## IncohĂ©rence de devise -## ✅ Ce que vous y gagnez +**Code :** `AMOUNT_CURRENCY_MISMATCH` -Avec FirstClassErrors : +Cette erreur survient lorsqu’une opĂ©ration combine des montants exprimĂ©s dans des devises diffĂ©rentes. -* les erreurs sont cohĂ©rentes -* la documentation est proche du code -* les diagnostics guident le dĂ©pannage -* la connaissance ne dĂ©rive pas - -Vous passez de : +**RĂšgle :** Une opĂ©ration monĂ©taire doit utiliser une devise commune. +``` -> « Une exception s’est produite » +Commitez ou publiez ce catalogue selon votre workflow de livraison. Pour automatiser sa gĂ©nĂ©ration en CI, consultez [IntĂ©gration CI/CD et exploitation](OperationalIntegration.fr.md). -Ă  +## Étapes suivantes facultatives -> « Cette erreur prĂ©cise et documentĂ©e s’est produite, voici ce qu’elle signifie et oĂč chercher. » +- Ajoutez des faits propres Ă  chaque occurrence avec le [contexte d’erreur](ErrorContext.fr.md). +- DĂ©couvrez les catĂ©gories disponibles dans [Taxonomie et composition des erreurs](ErrorTaxonomy.fr.md). +- Apprenez quand lever ou retourner un `Outcome` dans les [cas d’usage](UsagePatterns.fr.md). +- ProtĂ©gez les codes et clĂ©s de contexte stables avec le [versionnage du catalogue](CatalogVersioning.fr.md). --- ---- \ No newline at end of file +--- From ac6e653d0ad4f078b96371c0be5b757d844fc8b6 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:47:05 +0200 Subject: [PATCH 05/19] docs: make design principles concrete --- doc/DesignPrinciples.en.md | 91 ++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/doc/DesignPrinciples.en.md b/doc/DesignPrinciples.en.md index e722d08..b0acb10 100644 --- a/doc/DesignPrinciples.en.md +++ b/doc/DesignPrinciples.en.md @@ -3,24 +3,97 @@ 🌍 **Languages:** 🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./DesignPrinciples.fr.md) -FirstClassErrors is built on the idea that errors are not accidental by-products of code, but meaningful parts of the system’s knowledge. In many applications, errors are treated as technical noise — something to log, catch, or hide. This library takes the opposite stance: when an error is expressed, it reveals something about the rules, assumptions, and boundaries of the system. +FirstClassErrors is built around five principles. Each one has a direct consequence in the API and in the way errors are written. -An **error** is not merely a failure of execution. It represents a situation the system recognizes and gives a name to. By turning error situations into explicit concepts — through factory methods, codes, diagnostics, and documentation — the system becomes more readable, more explainable, and more supportable. +## 1. An error is a recognized situation -Another core principle is that documentation must not drift away from behavior. Traditional documentation lives in external files and slowly becomes outdated. Here, documentation is defined next to the code that creates the error. This proximity ensures that knowledge evolves with the system itself. If behavior changes, the documentation changes with it, because they share the same source. +An error is not merely a message emitted after something failed. It is a situation the system recognizes and gives a stable name. -Diagnostics are not post-mortem analysis; they are structured hypotheses. The goal is not to assign blame or determine root causes in advance, but to provide meaningful starting points for investigation. Errors describe what is known, not what is assumed. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -The library also separates semantics from mechanics. Throwing, catching, logging, or transporting errors are mechanical concerns. The meaning of an error — what rule was violated, what situation occurred, what might explain it — belongs to the domain of knowledge. FirstClassErrors focuses on preserving that meaning, regardless of how the error travels through the system. +The factory name expresses the situation for humans. Its `ErrorCode` gives the same situation a stable identity for clients, logs, dashboards, and documentation. -Finally, the design acknowledges that not every failure should be exceptional in the runtime sense. Some errors are expected parts of normal flow, such as validation failures or parsing issues. By allowing errors to be carried as structured data through `Outcome` instead of thrown, the model supports both throwing and non-throwing flows without losing semantic richness. +**Consequence:** one factory represents one precise error situation. Avoid generic factories such as `InvalidOperation(...)` that hide several unrelated failures behind one name. -In essence, the library encourages teams to treat errors as first-class knowledge artifacts. When errors are explicit, documented, and structured, they improve communication between developers, support teams, and the system itself. +## 2. The error is the model; the exception is a transport + +Throwing is one way to move an error through the system, not the definition of the error itself. + +```csharp +Error error = InvalidAmountOperationError.CurrencyMismatch(left, right); + +throw error.ToException(); +``` + +The same error can instead be carried as data: + +```csharp +return Outcome.Failure(error); +``` + +**Consequence:** model the meaning once, then choose the transport according to the flow. Use an exception when execution cannot continue normally; use `Outcome` when failure is expected and should be handled explicitly. + +## 3. Public and internal information must remain separate + +A useful diagnostic message often contains identifiers, offending values, or internal state. That information must not accidentally become an API response. + +FirstClassErrors separates: + +- public messages intended for users or API clients; +- an internal diagnostic message intended for logs, support, and developers. + +The staged builder enforces that distinction when the error is created. + +**Consequence:** public messages remain safe and controlled, while diagnostics can still contain the detail required for investigation. + +## 4. Documentation belongs beside behavior + +Documentation written far from the code eventually drifts. FirstClassErrors links each factory to structured documentation in the same class: + +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` + +The documentation method describes the situation, rule, diagnostic hypotheses, and executable examples. + +**Consequence:** changing an error situation naturally brings its construction and documentation into the same review. The catalog is generated from the code rather than maintained as a second source of truth. + +## 5. Diagnostics are hypotheses, not verdicts + +At the moment an error is defined, its exact root cause is often unknown. Useful documentation should therefore propose plausible explanations and investigation leads without assigning blame. + +Prefer: + +> The amounts reached the operation without being converted to one currency. + +Over: + +> The developer forgot to convert the amounts. + +**Consequence:** diagnostics describe observable states and direct investigation. They do not encode support procedures, claim certainty, or accuse a person or system. + +## The resulting model + +These principles work together: + +```mermaid +flowchart LR + A[Named factory] --> B[Structured Error] + B --> C[Exception] + B --> D[Outcome] + A --> E[Structured documentation] + E --> F[Generated catalog] +``` + +The factory defines one meaningful situation. The `Error` preserves that meaning whichever transport is chosen, and the linked documentation makes the same knowledge available outside the runtime flow. --- ---- \ No newline at end of file +--- From 2e08fd2e05df2fe5fdb3b1ed59e8e44ca03ce9b7 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:47:25 +0200 Subject: [PATCH 06/19] docs: make the French design principles concrete --- doc/DesignPrinciples.fr.md | 93 ++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/doc/DesignPrinciples.fr.md b/doc/DesignPrinciples.fr.md index 1d7e576..467a15a 100644 --- a/doc/DesignPrinciples.fr.md +++ b/doc/DesignPrinciples.fr.md @@ -1,26 +1,99 @@ # Principes de conception -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](./DesignPrinciples.en.md) | đŸ‡«đŸ‡· Français (ce fichier) -FirstClassErrors repose sur l’idĂ©e que les erreurs ne sont pas des sous-produits accidentels du code, mais des Ă©lĂ©ments porteurs de sens dans la connaissance du systĂšme. Dans de nombreuses applications, les erreurs sont traitĂ©es comme du bruit technique — quelque chose Ă  logger, attraper ou masquer. Cette bibliothĂšque adopte la position inverse : lorsqu’une erreur est exprimĂ©e, elle rĂ©vĂšle quelque chose sur les rĂšgles, les hypothĂšses et les frontiĂšres du systĂšme. +FirstClassErrors repose sur cinq principes. Chacun possĂšde une consĂ©quence directe dans l’API et dans la maniĂšre d’écrire les erreurs. -Une **erreur** n’est pas simplement un Ă©chec d’exĂ©cution. Elle reprĂ©sente une situation que le systĂšme reconnaĂźt et Ă  laquelle il donne un nom. En transformant les situations d’erreur en concepts explicites — via des mĂ©thodes factory, des codes, des diagnostics et de la documentation — le systĂšme devient plus lisible, plus explicable et plus facile Ă  maintenir. +## 1. Une erreur est une situation reconnue -Un autre principe fondamental est que la documentation ne doit pas dĂ©river du comportement. La documentation traditionnelle vit dans des fichiers externes et devient progressivement obsolĂšte. Ici, la documentation est dĂ©finie Ă  cĂŽtĂ© du code qui crĂ©e l’erreur. Cette proximitĂ© garantit que la connaissance Ă©volue avec le systĂšme lui-mĂȘme. Si le comportement change, la documentation change avec lui, car ils partagent la mĂȘme source. +Une erreur n’est pas seulement un message Ă©mis aprĂšs un Ă©chec. C’est une situation que le systĂšme reconnaĂźt et Ă  laquelle il donne un nom stable. -Les diagnostics ne sont pas une analyse post-mortem ; ce sont des hypothĂšses structurĂ©es. L’objectif n’est pas d’attribuer une faute ni de dĂ©terminer Ă  l’avance une cause racine, mais de fournir des points de dĂ©part pertinents pour l’investigation. Les erreurs dĂ©crivent ce qui est connu, pas ce qui est supposĂ©. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -La bibliothĂšque sĂ©pare Ă©galement la sĂ©mantique de la mĂ©canique. Lever, intercepter, logger ou transporter des erreurs sont des prĂ©occupations mĂ©caniques. Le sens d’une erreur — quelle rĂšgle a Ă©tĂ© violĂ©e, quelle situation s’est produite, ce qui pourrait l’expliquer — appartient au domaine de la connaissance. FirstClassErrors se concentre sur la prĂ©servation de ce sens, indĂ©pendamment de la maniĂšre dont l’erreur circule dans le systĂšme. +Le nom de la factory exprime la situation pour les humains. Son `ErrorCode` donne Ă  cette mĂȘme situation une identitĂ© stable pour les clients, les logs, les dashboards et la documentation. -Enfin, la conception reconnaĂźt que tous les Ă©checs ne doivent pas ĂȘtre exceptionnels au sens runtime. Certaines erreurs font partie du flux normal, comme les Ă©checs de validation ou les problĂšmes de parsing. En permettant de transporter les erreurs comme donnĂ©es structurĂ©es via `Outcome` plutĂŽt que de les lever, le modĂšle supporte Ă  la fois les flux avec et sans levĂ©e d’exception, sans perdre la richesse sĂ©mantique. +**ConsĂ©quence :** une factory reprĂ©sente une situation d’erreur prĂ©cise. Évitez les factories gĂ©nĂ©riques comme `InvalidOperation(...)`, qui masquent plusieurs Ă©checs sans rapport derriĂšre un mĂȘme nom. -En essence, la bibliothĂšque encourage les Ă©quipes Ă  considĂ©rer les erreurs comme des artefacts de connaissance de premier plan. Lorsque les erreurs sont explicites, documentĂ©es et structurĂ©es, elles amĂ©liorent la communication entre les dĂ©veloppeurs, les Ă©quipes de support et le systĂšme lui-mĂȘme. +## 2. L’erreur est le modĂšle ; l’exception est un transport + +Lever une exception est une maniĂšre de faire circuler une erreur dans le systĂšme, pas la dĂ©finition de l’erreur elle-mĂȘme. + +```csharp +Error error = InvalidAmountOperationError.CurrencyMismatch(left, right); + +throw error.ToException(); +``` + +La mĂȘme erreur peut ĂȘtre transportĂ©e comme une donnĂ©e : + +```csharp +return Outcome.Failure(error); +``` + +**ConsĂ©quence :** modĂ©lisez le sens une seule fois, puis choisissez le transport selon le flux. Utilisez une exception lorsque l’exĂ©cution ne peut pas continuer normalement ; utilisez `Outcome` lorsque l’échec est attendu et doit ĂȘtre traitĂ© explicitement. + +## 3. Les informations publiques et internes doivent rester sĂ©parĂ©es + +Un message de diagnostic utile contient souvent des identifiants, des valeurs fautives ou un Ă©tat interne. Ces informations ne doivent pas devenir accidentellement une rĂ©ponse d’API. + +FirstClassErrors sĂ©pare : + +- les messages publics destinĂ©s aux utilisateurs ou clients d’API ; +- le message de diagnostic interne destinĂ© aux logs, au support et aux dĂ©veloppeurs. + +Le builder Ă©tagĂ© impose cette distinction lors de la crĂ©ation de l’erreur. + +**ConsĂ©quence :** les messages publics restent sĂ»rs et maĂźtrisĂ©s, tandis que les diagnostics peuvent conserver les dĂ©tails nĂ©cessaires Ă  l’investigation. + +## 4. La documentation doit vivre Ă  cĂŽtĂ© du comportement + +Une documentation Ă©crite loin du code finit par dĂ©river. FirstClassErrors relie chaque factory Ă  une documentation structurĂ©e situĂ©e dans la mĂȘme classe : + +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` + +La mĂ©thode de documentation dĂ©crit la situation, la rĂšgle, les hypothĂšses de diagnostic et des exemples exĂ©cutables. + +**ConsĂ©quence :** modifier une situation d’erreur amĂšne naturellement sa construction et sa documentation dans la mĂȘme revue. Le catalogue est gĂ©nĂ©rĂ© depuis le code au lieu d’ĂȘtre maintenu comme une seconde source de vĂ©ritĂ©. + +## 5. Les diagnostics sont des hypothĂšses, pas des verdicts + +Au moment oĂč une erreur est dĂ©finie, sa cause racine exacte est souvent inconnue. Une documentation utile doit donc proposer des explications plausibles et des pistes d’investigation sans attribuer de faute. + +PrĂ©fĂ©rez : + +> Les montants ont atteint l’opĂ©ration sans avoir Ă©tĂ© convertis dans une devise commune. + +À : + +> Le dĂ©veloppeur a oubliĂ© de convertir les montants. + +**ConsĂ©quence :** les diagnostics dĂ©crivent des Ă©tats observables et orientent l’investigation. Ils n’encodent pas de procĂ©dures de support, n’affirment pas une certitude et n’accusent ni une personne ni un systĂšme. + +## Le modĂšle qui en dĂ©coule + +Ces principes fonctionnent ensemble : + +```mermaid +flowchart LR + A[Factory nommĂ©e] --> B[Error structurĂ©e] + B --> C[Exception] + B --> D[Outcome] + A --> E[Documentation structurĂ©e] + E --> F[Catalogue gĂ©nĂ©rĂ©] +``` + +La factory dĂ©finit une situation significative. L’`Error` en prĂ©serve le sens quel que soit le transport choisi, et la documentation liĂ©e rend cette mĂȘme connaissance disponible en dehors du flux d’exĂ©cution. --- ---- \ No newline at end of file +--- From 20e368e129032d4eca7d3ced595d167537d76b2d Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:47:53 +0200 Subject: [PATCH 07/19] docs: focus core concepts on the error model --- doc/CoreConcepts.en.md | 194 +++++++++++++++++++++-------------------- 1 file changed, 99 insertions(+), 95 deletions(-) diff --git a/doc/CoreConcepts.en.md b/doc/CoreConcepts.en.md index be9b865..8035f71 100644 --- a/doc/CoreConcepts.en.md +++ b/doc/CoreConcepts.en.md @@ -3,151 +3,155 @@ 🌍 **Languages:** 🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./CoreConcepts.fr.md) -FirstClassErrors is not just a utility library. -It introduces a different way to think about application errors. +FirstClassErrors separates the **meaning of a failure** from the mechanism used to propagate it. -Instead of reducing a failure to a mere technical incident, it turns the **error** behind it into **structured knowledge about what went wrong** — while the exception stays the mechanism that signals and propagates that failure. +The central object is `Error`. An exception or an `Outcome` only transports that error. -## 🧠 An error is not just a message +```mermaid +flowchart LR + A[Error factory] --> B[Error] + B --> C[ToException] + B --> D[Outcome or Outcome of T] + A --> E[DocumentedBy] + E --> F[Generated documentation] +``` -In many systems, exceptions are reduced to: +## An error represents one situation -> a type + a string message +A useful error answers a precise question: -In FirstClassErrors, an **error** represents: +> What situation did the system recognize? -* a **specific error situation** -* identified by a **stable error code** -* described with three purpose-built messages (a public summary, an optional public detail, and an internal diagnostic message) -* optionally enriched with context -* associated with structured diagnostics +For example: -An **error** becomes a semantic object, not just a runtime signal. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -### Three messages, two audiences +The factory gives the situation a readable name. Its stable code gives the same situation a machine-readable identity: -An error carries three messages but deliberately splits them across just **two audiences** — a public one (end users / API clients) and an internal one (logs, support, developers). The separation is guaranteed by construction, so what reaches a caller can never leak what is meant for developers and support: +```text +AMOUNT_CURRENCY_MISMATCH +``` -* **`ShortMessage`** (mandatory) — a short public summary, safe to expose to an end user or an API client (e.g. the `title` of an RFC 9457 problem detail). -* **`DetailedMessage`** (optional) — a controlled public detail, exposable **only** when the application explicitly chooses to (e.g. the `detail` of an RFC 9457 problem detail). It must never carry sensitive or internal information. -* **`DiagnosticMessage`** (mandatory) — the internal diagnostic message for logs, support and developers. It may contain technical/operational detail (identifiers, offending values, internal state) and is **never** exposed to external clients by default. `error.ToException()` uses it as the exception's `Message`. +One factory should therefore correspond to one documented error case. -The core model stays HTTP-agnostic: the diagnostic message is never a default HTTP response body, and `type` and `status` remain the application's concern. When an error is exposed over HTTP, its `Code` is the natural RFC 9457 `type`: surface it as a stable URI such as `urn:problem:{service}:{code}`, where `{code}` is the error code in lowercase kebab-case — for example `urn:problem:temperature-simulator:temperature-below-absolute-zero` or `urn:problem:banking-api:money-transfer-amount-not-positive` — so clients can branch on the problem type. +## What an `Error` carries -## đŸ§© A factory represents an error situation +An error occurrence contains: -**Error** factories are central to the model. +- a stable `Code`; +- a unique `InstanceId`; +- an `OccurredAt` timestamp; +- public and internal messages; +- optional typed context; +- optional inner errors. -A factory method: +The factory centralizes how these values are created, so every occurrence of the same situation remains consistent. -* represents one precise error scenario -* gives it a **name** in the code -* centralizes error creation -* becomes the anchor for documentation +## Three messages, two audiences -This means: +An error carries three messages, divided between a public audience and an internal audience. -> Each factory = one documented error case. +| Message | Mandatory | Audience | Purpose | +| --- | --- | --- | --- | +| `ShortMessage` | yes | users and API clients | safe public summary | +| `DetailedMessage` | no | users and API clients | optional controlled detail | +| `DiagnosticMessage` | yes | logs, support, developers | internal investigation detail | -Factories improve readability and make error situations explicit, while keeping construction details out of the business logic. +The separation is deliberate. A diagnostic message may contain identifiers, offending values, or internal state that must never be exposed to an external client by default. -## 📘 Documentation lives with the code +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Cannot add {left} and {right} because their currencies differ.") + .WithPublicMessage( + shortMessage: "The amounts use different currencies.", + detailedMessage: "Both amounts must use the same currency."); +``` -Error documentation is written using the `DescribeError` DSL and linked directly to **error** factories. +Calling `error.ToException()` uses the diagnostic message as the exception's `Message`. How public messages are mapped to HTTP, gRPC, a UI, or another transport remains the application's responsibility. -This creates: +## A factory is the source of truth -* structured descriptions -* violated rules -* diagnostics -* realistic examples +Factories keep construction details out of business logic: -Because documentation is code: +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); +} +``` -* it evolves with the system -* it does not drift -* it can be extracted automatically +The code states the recognized situation without repeating its code, messages, context, or construction rules. -This is **living documentation**. +Factories also anchor the documentation: -## 🔎 Diagnostics describe hypotheses, not blame +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` -Diagnostics answer: +The linked method describes the stable meaning of the situation: its title, explanation, rule, diagnostics, and representative examples. -* What might have caused this error? -* Is it likely input-related, system-related, or both? -* Where should investigation start? +## Documentation and runtime data are different -Diagnostics are: +The documentation describes the error **category**: -* structured -* human-oriented -* guidance for analysis +- what the situation means; +- which rule it represents; +- what might cause it; +- where investigation can start. -They do not encode operational processes. They provide **direction**, not procedures. +The runtime error describes one **occurrence**: -## 🧭 Error taxonomy +- when it happened; +- its unique instance id; +- the actual diagnostic message; +- occurrence-specific context values. -Errors are modeled as a hierarchy rooted in the abstract `Error` type: +For example, `ORDER_NOT_FOUND` is the stable category. `OrderId = 42` belongs to one occurrence and therefore belongs in `ErrorContext`. -* **`DomainError`** — a violation of a domain rule (the domain layer). -* **`InfrastructureError`** — a failure at a technical boundary. It carries a `Transience` (`Unknown` / `NonTransient` / `Transient`) and an `InteractionDirection`. - * **`PrimaryPortError`** — incoming boundary (`Direction` fixed to `Incoming`). - * **`SecondaryPortError`** — outgoing boundary (`Direction` fixed to `Outgoing`). +## Diagnostics guide investigation -The Port errors replace the old Adapter exceptions. When a port failure wraps several causes, `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` aggregate the inner errors and compute the overall transience. +A diagnostic is a structured hypothesis composed of: -**Nesting rules.** Inner errors capture *causes*, and the model enforces what may nest in what — by construction, not by convention: +- a possible cause; +- its likely `ErrorOrigin` (`Internal`, `External`, or `InternalOrExternal`); +- an analysis lead. -* A **`DomainError`** nests **only** other `DomainError`s. A domain failure is only ever caused by — or aggregated from — other domain failures; it never carries an infrastructure cause (that would leak a technical concern into the domain vocabulary). -* A **`PrimaryPortError`** / **`SecondaryPortError`** nests **infrastructure errors of its own direction** (a primary port nests primary-port errors, a secondary port nests secondary-port errors) **and/or** `DomainError`s — for example a boundary rejection whose cause is a domain-invariant violation surfaced while mapping an incoming request. The typed `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` builders make anything else unrepresentable: they only expose `Add(DomainError)` and `Add(`_same-direction port error_`)`. -* The base **`InfrastructureError`** is the permissive general case and accepts any `Error` as inner. Prefer the Port types in real code: they pin the `InteractionDirection` and keep nesting consistent. +Diagnostics should not claim a root cause that is not yet known. They should describe plausible states and suggest what to verify first. -In short: **a domain error contains only domain errors; an infrastructure error contains same-direction infrastructure errors and/or domain errors.** The [FAQ](FAQ.en.md) explains *why* this asymmetry exists. +## One model, two common transports -Each error has a paired exception reached via `error.ToException()`: `DomainException`, `InfrastructureException`, `PrimaryPortException`, `SecondaryPortException`. You never `new` these directly; the exception exposes its `Error` (and through it the context and inner errors). +When the system cannot continue normally, throw the paired exception: -## 🔁 Error or data? Both are supported +```csharp +throw error.ToException(); +``` -Traditionally, exceptions are always thrown. -FirstClassErrors supports two complementary models: +When failure is expected and should remain explicit in the normal flow, return it as data: -* **Exception as control flow** (classic throw) -* **Error as data** (`Outcome`, or non-generic `Outcome` when there is no value) +```csharp +return Outcome.Failure(error); +``` -This allows errors to be: +The error does not change identity when its transport changes. This makes it possible to return an error from domain logic, log it, or escalate it later without recreating or translating the model. -* raised immediately -* transported through validation pipelines -* escalated later +## Error categories -The same error situation can serve both roles. +FirstClassErrors provides several categories for distinguishing domain rule violations from technical-boundary failures: -The non-throwing model is `Outcome` / `Outcome`: the `Error` is carried as data (`IsSuccess` / `IsFailure` / `Error`) and can be converted into an exception on demand via `error.ToException()`. +- `DomainError`; +- `InfrastructureError`; +- `PrimaryPortError`; +- `SecondaryPortError`. -## 🎯 From failures to knowledge - -With this model, errors are no longer: - -> isolated technical failures - -They become: - -> shared, structured knowledge about how the system can fail. - -This bridges: - -* development -* support -* documentation -* operations - -All based on the same source of truth: the code. +Their interaction direction, transience, and composition rules are explained separately in [Error Taxonomy and Composition](ErrorTaxonomy.en.md). --- ---- \ No newline at end of file +--- From e177f0cbc624a02a47c425bd89983ff9da3ad094 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:48:17 +0200 Subject: [PATCH 08/19] docs: focus the French core concepts on the error model --- doc/CoreConcepts.fr.md | 197 +++++++++++++++++++++-------------------- 1 file changed, 101 insertions(+), 96 deletions(-) diff --git a/doc/CoreConcepts.fr.md b/doc/CoreConcepts.fr.md index 5e8a6f1..df8b43d 100644 --- a/doc/CoreConcepts.fr.md +++ b/doc/CoreConcepts.fr.md @@ -1,152 +1,157 @@ -# Concepts clĂ©s +# Concepts fondamentaux -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](./CoreConcepts.en.md) | đŸ‡«đŸ‡· Français (ce fichier) -FirstClassErrors n’est pas simplement une bibliothĂšque utilitaire. Il introduit une autre maniĂšre de penser les erreurs applicatives. +FirstClassErrors sĂ©pare le **sens d’un Ă©chec** du mĂ©canisme utilisĂ© pour le propager. -Au lieu de rĂ©duire une dĂ©faillance Ă  un simple incident technique, elle fait de l’**erreur** qui la sous-tend **une connaissance structurĂ©e sur ce qui s’est mal passĂ©** — l’exception restant le mĂ©canisme qui la signale et la propage. +L’objet central est `Error`. Une exception ou un `Outcome` ne fait que transporter cette erreur. -## 🧠 Une erreur n’est pas juste un message +```mermaid +flowchart LR + A[Factory d’erreur] --> B[Error] + B --> C[ToException] + B --> D[Outcome ou Outcome de T] + A --> E[DocumentedBy] + E --> F[Documentation gĂ©nĂ©rĂ©e] +``` -Dans de nombreux systĂšmes, les exceptions se rĂ©sument Ă  : +## Une erreur reprĂ©sente une situation -> un type + un message texte +Une erreur utile rĂ©pond Ă  une question prĂ©cise : -Avec FirstClassErrors, une **erreur** reprĂ©sente : +> Quelle situation le systĂšme a-t-il reconnue ? -* une **situation d’erreur spĂ©cifique** -* identifiĂ©e par un **code d’erreur stable** -* dĂ©crite avec trois messages dĂ©diĂ©s (un rĂ©sumĂ© public, un dĂ©tail public optionnel et un message de diagnostic interne) -* Ă©ventuellement enrichie de contexte -* associĂ©e Ă  des diagnostics structurĂ©s +Par exemple : -Une **erreur** devient un objet sĂ©mantique, pas seulement un signal d’exĂ©cution. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -### Trois messages, deux publics +La factory donne Ă  la situation un nom lisible. Son code stable lui donne une identitĂ© lisible par machine : -Une erreur porte trois messages mais les rĂ©partit dĂ©libĂ©rĂ©ment entre seulement **deux publics** — un public externe (utilisateurs finaux / clients d’API) et un public interne (logs, support, dĂ©veloppeurs). La sĂ©paration est garantie par construction : ce qui atteint un appelant ne peut jamais laisser fuiter ce qui est destinĂ© aux dĂ©veloppeurs et au support : +```text +AMOUNT_CURRENCY_MISMATCH +``` -* **`ShortMessage`** (obligatoire) — un rĂ©sumĂ© public court, exposable sans risque Ă  un utilisateur final ou Ă  un client d’API (par ex. le `title` d’un problem detail RFC 9457). -* **`DetailedMessage`** (optionnel) — un dĂ©tail public maĂźtrisĂ©, exposable **uniquement** si l’application le dĂ©cide explicitement (par ex. le `detail` d’un problem detail RFC 9457). Il ne doit jamais contenir d’information sensible ou interne. -* **`DiagnosticMessage`** (obligatoire) — le message de diagnostic interne destinĂ© aux logs, au support et aux dĂ©veloppeurs. Il peut contenir des dĂ©tails techniques/opĂ©rationnels (identifiants, valeurs fautives, Ă©tat interne) et n’est **jamais** exposĂ© aux clients externes par dĂ©faut. `error.ToException()` l’utilise comme `Message` de l’exception. +Une factory doit donc correspondre Ă  un cas d’erreur documentĂ©. -Le cƓur du modĂšle reste agnostique vis-Ă -vis d’HTTP : le message de diagnostic n’est jamais un corps de rĂ©ponse HTTP par dĂ©faut, et `type` et `status` restent l’affaire de l’application. Lorsqu’une erreur est exposĂ©e en HTTP, son `Code` est le `type` RFC 9457 naturel : exposez-le comme un URI stable tel que `urn:problem:{service}:{code}`, oĂč `{code}` est le code d’erreur en minuscules et en kebab-case — par exemple `urn:problem:temperature-simulator:temperature-below-absolute-zero` ou `urn:problem:banking-api:money-transfer-amount-not-positive` — afin que les clients puissent aiguiller sur le type de problĂšme. +## Ce que porte une `Error` -## đŸ§© Une factory reprĂ©sente une situation d’erreur +Une occurrence d’erreur contient : -Les factories d’**erreur** sont au cƓur du modĂšle. +- un `Code` stable ; +- un `InstanceId` unique ; +- un horodatage `OccurredAt` ; +- des messages publics et internes ; +- un contexte typĂ© optionnel ; +- des erreurs internes optionnelles. -Une mĂ©thode factory : +La factory centralise la crĂ©ation de ces valeurs, afin que chaque occurrence d’une mĂȘme situation reste cohĂ©rente. -* reprĂ©sente un scĂ©nario d’erreur prĂ©cis -* lui donne un **nom** dans le code -* centralise la crĂ©ation de l’erreur -* devient le point d’ancrage de la documentation +## Trois messages, deux publics -Cela signifie : +Une erreur porte trois messages rĂ©partis entre un public externe et un public interne. -> Chaque factory = un cas d’erreur documentĂ©. +| Message | Obligatoire | Public | RĂŽle | +| --- | --- | --- | --- | +| `ShortMessage` | oui | utilisateurs et clients d’API | rĂ©sumĂ© public sĂ»r | +| `DetailedMessage` | non | utilisateurs et clients d’API | dĂ©tail public optionnel et maĂźtrisĂ© | +| `DiagnosticMessage` | oui | logs, support et dĂ©veloppeurs | dĂ©tail interne pour l’investigation | -Les factories amĂ©liorent la lisibilitĂ© et rendent explicites les situations d’erreur, tout en gardant les dĂ©tails de construction en dehors de la logique mĂ©tier. +Cette sĂ©paration est volontaire. Un message de diagnostic peut contenir des identifiants, des valeurs fautives ou un Ă©tat interne qui ne doivent jamais ĂȘtre exposĂ©s par dĂ©faut Ă  un client externe. -## 📘 La documentation vit avec le code +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Impossible d’additionner {left} et {right} car leurs devises diffĂšrent.") + .WithPublicMessage( + shortMessage: "Les montants utilisent des devises diffĂ©rentes.", + detailedMessage: "Les deux montants doivent utiliser la mĂȘme devise."); +``` -La documentation des erreurs est Ă©crite avec le DSL `DescribeError` et liĂ©e directement aux factories d’**erreur**. +`error.ToException()` utilise le message de diagnostic comme `Message` de l’exception. Le mapping des messages publics vers HTTP, gRPC, une interface utilisateur ou un autre transport reste la responsabilitĂ© de l’application. -Cela permet de dĂ©finir : +## La factory est la source de vĂ©ritĂ© -* des descriptions structurĂ©es -* les rĂšgles violĂ©es -* des diagnostics -* des exemples rĂ©alistes +Les factories gardent les dĂ©tails de construction hors de la logique mĂ©tier : -Comme la documentation est du code : +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); +} +``` -* elle Ă©volue avec le systĂšme -* elle ne dĂ©rive pas -* elle peut ĂȘtre extraite automatiquement +Le code nomme la situation reconnue sans rĂ©pĂ©ter son code, ses messages, son contexte ni ses rĂšgles de construction. -C’est de la **documentation vivante**. +Les factories servent Ă©galement de point d’ancrage Ă  la documentation : -## 🔎 Les diagnostics dĂ©crivent des hypothĂšses, pas des fautes +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` -Les diagnostics rĂ©pondent Ă  : +La mĂ©thode liĂ©e dĂ©crit le sens stable de la situation : son titre, son explication, sa rĂšgle, ses diagnostics et des exemples reprĂ©sentatifs. -* Qu’est-ce qui pourrait avoir causĂ© cette erreur ? -* Est-ce probablement liĂ© aux donnĂ©es d’entrĂ©e, au systĂšme, ou aux deux ? -* Par oĂč commencer l’investigation ? +## Documentation et donnĂ©es d’exĂ©cution sont diffĂ©rentes -Les diagnostics sont : +La documentation dĂ©crit la **catĂ©gorie** d’erreur : -* structurĂ©s -* orientĂ©s humains -* des guides pour l’analyse +- ce que signifie la situation ; +- quelle rĂšgle elle reprĂ©sente ; +- ce qui pourrait la provoquer ; +- par oĂč commencer l’investigation. -Ils n’encodent pas de processus opĂ©rationnels. Ils donnent une **direction**, pas des procĂ©dures. +L’erreur d’exĂ©cution dĂ©crit une **occurrence** : -## 🧭 Taxonomie des erreurs +- quand elle s’est produite ; +- son identifiant unique ; +- son message de diagnostic rĂ©el ; +- les valeurs de contexte propres Ă  cette occurrence. -Les erreurs sont modĂ©lisĂ©es sous forme de hiĂ©rarchie ayant pour racine le type abstrait `Error` : +Par exemple, `ORDER_NOT_FOUND` est la catĂ©gorie stable. `OrderId = 42` appartient Ă  une occurrence prĂ©cise et doit donc ĂȘtre placĂ© dans `ErrorContext`. -* **`DomainError`** — une violation d’une rĂšgle mĂ©tier (la couche domaine). -* **`InfrastructureError`** — une dĂ©faillance Ă  une frontiĂšre technique. Elle porte une `Transience` (`Unknown` / `NonTransient` / `Transient`) et une `InteractionDirection`. - * **`PrimaryPortError`** — frontiĂšre entrante (`Direction` fixĂ©e Ă  `Incoming`). - * **`SecondaryPortError`** — frontiĂšre sortante (`Direction` fixĂ©e Ă  `Outgoing`). +## Les diagnostics orientent l’investigation -Les erreurs de Port remplacent les anciennes exceptions d’Adapter. Lorsqu’une dĂ©faillance de port enveloppe plusieurs causes, `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` agrĂšgent les erreurs internes et calculent la transience globale. +Un diagnostic est une hypothĂšse structurĂ©e composĂ©e de : -**RĂšgles d’imbrication.** Les erreurs internes capturent des *causes*, et le modĂšle contraint ce qui peut s’imbriquer dans quoi — par construction, pas par convention : +- une cause possible ; +- son `ErrorOrigin` probable (`Internal`, `External` ou `InternalOrExternal`) ; +- une piste d’analyse. -* Une **`DomainError`** n’imbrique **que** d’autres `DomainError`. Une dĂ©faillance mĂ©tier n’a jamais pour cause — ou pour agrĂ©gat — que d’autres dĂ©faillances mĂ©tier ; elle ne porte jamais une cause d’infrastructure (ce serait faire fuiter une prĂ©occupation technique dans le vocabulaire du domaine). -* Une **`PrimaryPortError`** / **`SecondaryPortError`** imbrique des **erreurs d’infrastructure de sa propre direction** (un port primaire imbrique des erreurs de port primaire, un port secondaire des erreurs de port secondaire) **et/ou** des `DomainError` — par exemple un rejet Ă  la frontiĂšre dont la cause est une violation d’invariant mĂ©tier dĂ©tectĂ©e lors du mapping d’une requĂȘte entrante. Les collections typĂ©es `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` rendent tout le reste non reprĂ©sentable : elles n’exposent que `Add(DomainError)` et `Add(`_erreur de port de mĂȘme direction_`)`. -* La base **`InfrastructureError`** est le cas gĂ©nĂ©ral permissif et accepte n’importe quelle `Error` comme cause interne. Dans du vrai code, prĂ©fĂšre les types de Port : ils fixent l’`InteractionDirection` et gardent l’imbrication cohĂ©rente. +Les diagnostics ne doivent pas affirmer une cause racine qui n’est pas encore connue. Ils dĂ©crivent des Ă©tats plausibles et suggĂšrent ce qu’il faut vĂ©rifier en premier. -En rĂ©sumĂ© : **une erreur de domaine ne contient que des erreurs de domaine ; une erreur d’infrastructure contient des erreurs d’infrastructure de mĂȘme direction et/ou des erreurs de domaine.** La [FAQ](FAQ.fr.md) explique *pourquoi* cette asymĂ©trie existe. +## Un modĂšle, deux transports courants -Chaque erreur possĂšde une exception associĂ©e, obtenue via `error.ToException()` : `DomainException`, `InfrastructureException`, `PrimaryPortException`, `SecondaryPortException`. On ne les instancie jamais directement avec `new` ; l’exception expose son `Error` (et, Ă  travers lui, le contexte et les erreurs internes). +Lorsque le systĂšme ne peut pas continuer normalement, levez l’exception associĂ©e : -## 🔁 Erreur ou donnĂ©e ? Les deux sont possibles +```csharp +throw error.ToException(); +``` -Traditionnellement, les exceptions sont toujours levĂ©es. -FirstClassErrors supporte deux modĂšles complĂ©mentaires : +Lorsque l’échec est attendu et doit rester explicite dans le flux normal, retournez-le comme une donnĂ©e : -* **L’exception comme flux de contrĂŽle** (throw classique) -* **L’erreur comme donnĂ©e** (`Outcome`, ou `Outcome` non gĂ©nĂ©rique lorsqu’il n’y a pas de valeur) +```csharp +return Outcome.Failure(error); +``` -Cela permet aux erreurs d’ĂȘtre : +L’erreur ne change pas d’identitĂ© lorsque son transport change. Elle peut donc ĂȘtre retournĂ©e depuis la logique mĂ©tier, journalisĂ©e ou transformĂ©e en exception plus tard sans recrĂ©er ni traduire le modĂšle. -* levĂ©es immĂ©diatement -* transportĂ©es dans des pipelines de validation -* escaladĂ©es plus tard +## CatĂ©gories d’erreur -La mĂȘme situation d’erreur peut servir ces deux rĂŽles. +FirstClassErrors fournit plusieurs catĂ©gories pour distinguer les violations de rĂšgles mĂ©tier des dĂ©faillances aux frontiĂšres techniques : -Le modĂšle sans levĂ©e d’exception est `Outcome` / `Outcome` : l’`Error` est portĂ©e comme donnĂ©e (`IsSuccess` / `IsFailure` / `Error`) et peut ĂȘtre convertie en exception Ă  la demande via `error.ToException()`. +- `DomainError` ; +- `InfrastructureError` ; +- `PrimaryPortError` ; +- `SecondaryPortError`. -## 🎯 De l’échec Ă  la connaissance - -Avec ce modĂšle, les erreurs ne sont plus : - -> des dĂ©faillances techniques isolĂ©es - -Elles deviennent : - -> une connaissance partagĂ©e et structurĂ©e sur la maniĂšre dont le systĂšme peut Ă©chouer. - -Cela crĂ©e un pont entre : - -* le dĂ©veloppement -* le support -* la documentation -* l’exploitation - -Le tout basĂ© sur une mĂȘme source de vĂ©ritĂ© : le code. +Leur direction d’interaction, leur transience et leurs rĂšgles de composition sont expliquĂ©es sĂ©parĂ©ment dans [Taxonomie et composition des erreurs](ErrorTaxonomy.fr.md). --- ---- \ No newline at end of file +--- From 155abdbb644681e5836cda994543e821ec4c903a Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:48:49 +0200 Subject: [PATCH 09/19] docs: add error taxonomy guide --- doc/ErrorTaxonomy.en.md | 154 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 doc/ErrorTaxonomy.en.md diff --git a/doc/ErrorTaxonomy.en.md b/doc/ErrorTaxonomy.en.md new file mode 100644 index 0000000..e9c712c --- /dev/null +++ b/doc/ErrorTaxonomy.en.md @@ -0,0 +1,154 @@ +# Error Taxonomy and Composition + +🌍 **Languages:** +🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./ErrorTaxonomy.fr.md) + +FirstClassErrors distinguishes the **nature of a failure** from the place where it happens to be detected. + +That distinction helps domain code remain independent from infrastructure while giving operational tooling useful signals such as interaction direction and transience. + +## The hierarchy at a glance + +```mermaid +classDiagram + Error <|-- DomainError + Error <|-- InfrastructureError + InfrastructureError <|-- PrimaryPortError + InfrastructureError <|-- SecondaryPortError +``` + +| Type | Meaning | Typical example | +| --- | --- | --- | +| `DomainError` | a domain rule or invariant was violated | an amount uses an unsupported currency | +| `InfrastructureError` | a technical interaction failed, without a more precise port category | a generic infrastructure component failed | +| `PrimaryPortError` | an incoming interaction could not be accepted or processed | an API request cannot be mapped into valid domain values | +| `SecondaryPortError` | an outgoing interaction failed | a database or remote service is unavailable | + +## `DomainError`: the business rule was violated + +Use a `DomainError` when the failure is expressed entirely in domain language. + +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Cannot combine {left.Currency} and {right.Currency} amounts.") + .WithPublicMessage( + shortMessage: "The amounts use different currencies."); +``` + +The same error remains a domain error whether it is detected in a value object, an entity, a domain service, or while an adapter is constructing a domain value. + +The type follows **which rule was violated**, not which layer happened to observe it. + +## `PrimaryPortError`: an incoming interaction failed + +A primary port represents an interaction entering the application: HTTP, messaging, a CLI command, a file import, or another inbound adapter. + +Consider an API request containing an invalid amount. Two facts may need to be preserved: + +1. the domain rejected the value because an invariant was violated; +2. the incoming request therefore cannot be accepted. + +The adapter can wrap the domain cause in a primary-port error: + +```csharp +DomainError invalidAmount = InvalidAmountError.NegativeValue(request.Amount); + +return PrimaryPortError.Create( + Code.RequestRejected, + diagnosticMessage: $"Request {request.Id} contains an invalid amount.", + transience: Transience.NonTransient, + configureInnerErrors: inner => inner.Add(invalidAmount)) + .WithPublicMessage( + shortMessage: "The request contains invalid data."); +``` + +The domain error still describes the violated business rule. The primary-port error describes the boundary condition: this incoming interaction is rejected. + +## `SecondaryPortError`: an outgoing interaction failed + +A secondary port represents an interaction initiated by the application toward a database, broker, filesystem, or remote service. + +```csharp +return SecondaryPortError.Create( + Code.PaymentProviderUnavailable, + diagnosticMessage: "The payment provider timed out after 5 seconds.", + transience: Transience.Transient) + .WithPublicMessage( + shortMessage: "The payment service is temporarily unavailable."); +``` + +The outgoing direction and transient classification provide operational meaning that a generic exception type or message would not preserve. + +## `Transience`: is retrying meaningful? + +Infrastructure errors carry a `Transience` value: + +| Value | Meaning | +| --- | --- | +| `Transient` | the same operation may succeed later without changing the request | +| `NonTransient` | retrying the same operation is not expected to help | +| `Unknown` | the system cannot classify the failure reliably | + +Examples: + +- database timeout → usually `Transient`; +- unsupported request format → `NonTransient`; +- unclassified third-party failure → `Unknown`. + +Transience is an operational hint, not a retry policy. The application still decides whether, when, and how often to retry. + +## Interaction direction + +Port errors fix their direction by construction: + +- `PrimaryPortError` → `Incoming`; +- `SecondaryPortError` → `Outgoing`. + +This allows logs and monitoring to distinguish a rejected inbound request from a failing outbound dependency, even when both are non-transient. + +For example, an invalid email entered by a user should not trigger the same alert as a database outage. Direction and transience preserve that distinction. + +## Composition rules + +Inner errors represent causes or aggregated failures. The model constrains what may be nested: + +| Outer error | Allowed inner errors | +| --- | --- | +| `DomainError` | `DomainError` only | +| `PrimaryPortError` | `DomainError` and `PrimaryPortError` | +| `SecondaryPortError` | `DomainError` and `SecondaryPortError` | +| base `InfrastructureError` | any `Error` | + +These rules prevent technical concerns from leaking into domain vocabulary and prevent an incoming port error from accidentally containing an unrelated outgoing-port classification. + +### Why a domain error cannot contain an infrastructure error + +A `DomainError` states that a business rule was violated. If it contained a database timeout as its cause, the model would describe a technical outage as though it were part of the business rule. + +Keep the domain failure and the technical failure distinct. If the actual failure is infrastructural, represent it with an infrastructure or port error at the boundary that owns that interaction. + +### Why a port error may contain a domain error + +A boundary may legitimately reject an interaction because domain construction failed. The domain error explains the underlying rule; the port error explains the boundary-level outcome. + +This preserves both facts without making the domain depend on HTTP, messaging, files, or any other adapter technology. + +## Choosing the type + +Ask these questions in order: + +1. **Was a business rule or invariant violated?** Use `DomainError`. +2. **Did an incoming interaction fail at the application boundary?** Use `PrimaryPortError`. +3. **Did an outgoing dependency interaction fail?** Use `SecondaryPortError`. +4. **Is it infrastructural but not meaningfully classified by direction?** Use `InfrastructureError`. + +Do not choose a type from the current class or folder. Choose it from the meaning of the failure. + +--- + + + +--- From 08ff3865e3b06c1cdedd333686459a4b954e99bf Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:49:17 +0200 Subject: [PATCH 10/19] docs: add the French error taxonomy guide --- doc/ErrorTaxonomy.fr.md | 154 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 doc/ErrorTaxonomy.fr.md diff --git a/doc/ErrorTaxonomy.fr.md b/doc/ErrorTaxonomy.fr.md new file mode 100644 index 0000000..1f54ad7 --- /dev/null +++ b/doc/ErrorTaxonomy.fr.md @@ -0,0 +1,154 @@ +# Taxonomie et composition des erreurs + +🌍 **Langues :** +🇬🇧 [English](./ErrorTaxonomy.en.md) | đŸ‡«đŸ‡· Français (ce fichier) + +FirstClassErrors distingue la **nature d’un Ă©chec** de l’endroit oĂč il est dĂ©tectĂ©. + +Cette distinction permet au domaine de rester indĂ©pendant de l’infrastructure, tout en fournissant Ă  l’exploitation des signaux utiles comme la direction de l’interaction et la transience. + +## La hiĂ©rarchie en un coup d’Ɠil + +```mermaid +classDiagram + Error <|-- DomainError + Error <|-- InfrastructureError + InfrastructureError <|-- PrimaryPortError + InfrastructureError <|-- SecondaryPortError +``` + +| Type | Signification | Exemple courant | +| --- | --- | --- | +| `DomainError` | une rĂšgle ou un invariant mĂ©tier a Ă©tĂ© violĂ© | un montant utilise une devise incompatible | +| `InfrastructureError` | une interaction technique a Ă©chouĂ© sans catĂ©gorie de port plus prĂ©cise | un composant d’infrastructure gĂ©nĂ©rique Ă©choue | +| `PrimaryPortError` | une interaction entrante ne peut pas ĂȘtre acceptĂ©e ou traitĂ©e | une requĂȘte API ne peut pas ĂȘtre convertie en valeurs mĂ©tier valides | +| `SecondaryPortError` | une interaction sortante a Ă©chouĂ© | une base de donnĂ©es ou un service distant est indisponible | + +## `DomainError` : une rĂšgle mĂ©tier a Ă©tĂ© violĂ©e + +Utilisez une `DomainError` lorsque l’échec s’exprime entiĂšrement dans le langage du domaine. + +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Impossible de combiner des montants en {left.Currency} et {right.Currency}.") + .WithPublicMessage( + shortMessage: "Les montants utilisent des devises diffĂ©rentes."); +``` + +La mĂȘme erreur reste une erreur de domaine, qu’elle soit dĂ©tectĂ©e dans un value object, une entitĂ©, un service de domaine ou pendant qu’un adapter construit une valeur mĂ©tier. + +Le type dĂ©pend de **la rĂšgle violĂ©e**, pas de la couche qui constate l’échec. + +## `PrimaryPortError` : une interaction entrante a Ă©chouĂ© + +Un port primaire reprĂ©sente une interaction qui entre dans l’application : HTTP, message, commande CLI, import de fichier ou autre adapter entrant. + +Prenons une requĂȘte API contenant un montant invalide. Deux faits peuvent devoir ĂȘtre conservĂ©s : + +1. le domaine a refusĂ© la valeur parce qu’un invariant a Ă©tĂ© violĂ© ; +2. la requĂȘte entrante ne peut donc pas ĂȘtre acceptĂ©e. + +L’adapter peut encapsuler la cause mĂ©tier dans une erreur de port primaire : + +```csharp +DomainError invalidAmount = InvalidAmountError.NegativeValue(request.Amount); + +return PrimaryPortError.Create( + Code.RequestRejected, + diagnosticMessage: $"La requĂȘte {request.Id} contient un montant invalide.", + transience: Transience.NonTransient, + configureInnerErrors: inner => inner.Add(invalidAmount)) + .WithPublicMessage( + shortMessage: "La requĂȘte contient des donnĂ©es invalides."); +``` + +L’erreur de domaine dĂ©crit toujours la rĂšgle violĂ©e. L’erreur de port primaire dĂ©crit la condition Ă  la frontiĂšre : cette interaction entrante est rejetĂ©e. + +## `SecondaryPortError` : une interaction sortante a Ă©chouĂ© + +Un port secondaire reprĂ©sente une interaction initiĂ©e par l’application vers une base de donnĂ©es, un broker, un systĂšme de fichiers ou un service distant. + +```csharp +return SecondaryPortError.Create( + Code.PaymentProviderUnavailable, + diagnosticMessage: "Le fournisseur de paiement a dĂ©passĂ© le dĂ©lai de 5 secondes.", + transience: Transience.Transient) + .WithPublicMessage( + shortMessage: "Le service de paiement est temporairement indisponible."); +``` + +La direction sortante et la classification transiente apportent un sens opĂ©rationnel qu’un type d’exception ou un message gĂ©nĂ©rique ne conserverait pas. + +## `Transience` : retenter peut-il ĂȘtre utile ? + +Les erreurs d’infrastructure portent une valeur `Transience` : + +| Valeur | Signification | +| --- | --- | +| `Transient` | la mĂȘme opĂ©ration peut rĂ©ussir plus tard sans modifier la requĂȘte | +| `NonTransient` | retenter la mĂȘme opĂ©ration ne devrait pas aider | +| `Unknown` | le systĂšme ne peut pas classer l’échec de façon fiable | + +Exemples : + +- timeout de base de donnĂ©es → gĂ©nĂ©ralement `Transient` ; +- format de requĂȘte non supportĂ© → `NonTransient` ; +- Ă©chec tiers non classĂ© → `Unknown`. + +La transience est un indice opĂ©rationnel, pas une politique de retry. L’application dĂ©cide toujours si, quand et combien de fois elle retente. + +## Direction de l’interaction + +Les erreurs de port fixent leur direction par construction : + +- `PrimaryPortError` → `Incoming` ; +- `SecondaryPortError` → `Outgoing`. + +Les logs et la supervision peuvent ainsi distinguer une requĂȘte entrante rejetĂ©e d’une dĂ©pendance sortante en panne, mĂȘme si les deux erreurs sont non transientes. + +Par exemple, une adresse e-mail invalide saisie par un utilisateur ne doit pas dĂ©clencher la mĂȘme alerte qu’une indisponibilitĂ© de base de donnĂ©es. La direction et la transience prĂ©servent cette diffĂ©rence. + +## RĂšgles de composition + +Les erreurs internes reprĂ©sentent des causes ou des Ă©checs agrĂ©gĂ©s. Le modĂšle contraint ce qui peut ĂȘtre imbriquĂ© : + +| Erreur externe | Erreurs internes autorisĂ©es | +| --- | --- | +| `DomainError` | uniquement des `DomainError` | +| `PrimaryPortError` | des `DomainError` et `PrimaryPortError` | +| `SecondaryPortError` | des `DomainError` et `SecondaryPortError` | +| `InfrastructureError` de base | n’importe quelle `Error` | + +Ces rĂšgles empĂȘchent les prĂ©occupations techniques de fuiter dans le vocabulaire mĂ©tier et Ă©vitent qu’une erreur de port entrant contienne accidentellement une classification de port sortant sans rapport. + +### Pourquoi une erreur de domaine ne contient-elle pas une erreur d’infrastructure ? + +Une `DomainError` affirme qu’une rĂšgle mĂ©tier a Ă©tĂ© violĂ©e. Si elle contenait un timeout de base de donnĂ©es comme cause, le modĂšle dĂ©crirait une panne technique comme si elle faisait partie de la rĂšgle mĂ©tier. + +Gardez l’échec mĂ©tier et l’échec technique distincts. Lorsque l’échec rĂ©el est infrastructurel, reprĂ©sentez-le par une erreur d’infrastructure ou de port Ă  la frontiĂšre qui possĂšde cette interaction. + +### Pourquoi une erreur de port peut-elle contenir une erreur de domaine ? + +Une frontiĂšre peut lĂ©gitimement rejeter une interaction parce que la construction d’un objet mĂ©tier a Ă©chouĂ©. L’erreur de domaine explique la rĂšgle sous-jacente ; l’erreur de port explique le rĂ©sultat au niveau de la frontiĂšre. + +Les deux faits sont prĂ©servĂ©s sans rendre le domaine dĂ©pendant de HTTP, de la messagerie, des fichiers ou d’une autre technologie d’adapter. + +## Choisir le type + +Posez ces questions dans l’ordre : + +1. **Une rĂšgle ou un invariant mĂ©tier a-t-il Ă©tĂ© violĂ© ?** Utilisez `DomainError`. +2. **Une interaction entrante a-t-elle Ă©chouĂ© Ă  la frontiĂšre de l’application ?** Utilisez `PrimaryPortError`. +3. **Une interaction avec une dĂ©pendance sortante a-t-elle Ă©chouĂ© ?** Utilisez `SecondaryPortError`. +4. **L’échec est-il infrastructurel sans direction pertinente ?** Utilisez `InfrastructureError`. + +Ne choisissez pas un type selon la classe ou le dossier actuel. Choisissez-le selon le sens de l’échec. + +--- + + + +--- From 2fd0eebe55f321379711f2ebd907ab4fcd867e3a Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:50:04 +0200 Subject: [PATCH 11/19] docs: use the real inner error API in taxonomy example --- doc/ErrorTaxonomy.en.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ErrorTaxonomy.en.md b/doc/ErrorTaxonomy.en.md index e9c712c..d662063 100644 --- a/doc/ErrorTaxonomy.en.md +++ b/doc/ErrorTaxonomy.en.md @@ -53,17 +53,17 @@ The adapter can wrap the domain cause in a primary-port error: ```csharp DomainError invalidAmount = InvalidAmountError.NegativeValue(request.Amount); +var innerErrors = new PrimaryPortInnerErrors().Add(invalidAmount); return PrimaryPortError.Create( Code.RequestRejected, diagnosticMessage: $"Request {request.Id} contains an invalid amount.", - transience: Transience.NonTransient, - configureInnerErrors: inner => inner.Add(invalidAmount)) + innerErrors: innerErrors) .WithPublicMessage( shortMessage: "The request contains invalid data."); ``` -The domain error still describes the violated business rule. The primary-port error describes the boundary condition: this incoming interaction is rejected. +The domain error still describes the violated business rule. The primary-port error describes the boundary condition: this incoming interaction is rejected. When inner errors are supplied, the port error computes its overall transience from them. ## `SecondaryPortError`: an outgoing interaction failed From b1d8aeb48c6acd344a5289ae85126666e0c2488b Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Tue, 14 Jul 2026 11:50:35 +0200 Subject: [PATCH 12/19] docs: use the real inner error API in the French taxonomy example --- doc/ErrorTaxonomy.fr.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ErrorTaxonomy.fr.md b/doc/ErrorTaxonomy.fr.md index 1f54ad7..da6a451 100644 --- a/doc/ErrorTaxonomy.fr.md +++ b/doc/ErrorTaxonomy.fr.md @@ -53,17 +53,17 @@ L’adapter peut encapsuler la cause mĂ©tier dans une erreur de port primaire : ```csharp DomainError invalidAmount = InvalidAmountError.NegativeValue(request.Amount); +var innerErrors = new PrimaryPortInnerErrors().Add(invalidAmount); return PrimaryPortError.Create( Code.RequestRejected, diagnosticMessage: $"La requĂȘte {request.Id} contient un montant invalide.", - transience: Transience.NonTransient, - configureInnerErrors: inner => inner.Add(invalidAmount)) + innerErrors: innerErrors) .WithPublicMessage( shortMessage: "La requĂȘte contient des donnĂ©es invalides."); ``` -L’erreur de domaine dĂ©crit toujours la rĂšgle violĂ©e. L’erreur de port primaire dĂ©crit la condition Ă  la frontiĂšre : cette interaction entrante est rejetĂ©e. +L’erreur de domaine dĂ©crit toujours la rĂšgle violĂ©e. L’erreur de port primaire dĂ©crit la condition Ă  la frontiĂšre : cette interaction entrante est rejetĂ©e. Lorsque des erreurs internes sont fournies, l’erreur de port calcule sa transience globale Ă  partir d’elles. ## `SecondaryPortError` : une interaction sortante a Ă©chouĂ© From 00fda56ef657bba24603695ae0b16e55058eee9a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 21:19:40 +0000 Subject: [PATCH 13/19] docs: require the diagnostics stage in the readme example The documentation method shown in the English and French readmes chained WithRule directly into WithExamples, which does not compile: the staged builder makes the diagnostics stage mandatory between the rule and the examples. Add the WithDiagnostic call, mirroring the getting started guide and the usage project. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LUaMS5EHzAdQHBJzrYvKTj --- README.md | 4 ++++ doc/README.fr.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 202a37f..db3bc94 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ public static class InvalidAmountOperationError { return DescribeError.WithTitle("Amount currency mismatch") .WithDescription("This error occurs when an operation combines amounts expressed in different currencies.") .WithRule("A monetary operation must use one common currency.") + .WithDiagnostic( + "The amounts reached the operation without being converted to one currency.", + ErrorOrigin.Internal, + "Verify where the amounts should have been converted before this operation.") .WithExamples(() => CurrencyMismatch( new Amount(10, Currency.EUR), new Amount(12, Currency.USD))); diff --git a/doc/README.fr.md b/doc/README.fr.md index 78481dd..0682b5b 100644 --- a/doc/README.fr.md +++ b/doc/README.fr.md @@ -61,6 +61,10 @@ public static class InvalidAmountOperationError { return DescribeError.WithTitle("IncohĂ©rence de devise") .WithDescription("Cette erreur survient lorsqu’une opĂ©ration combine des montants exprimĂ©s dans des devises diffĂ©rentes.") .WithRule("Une opĂ©ration monĂ©taire doit utiliser une devise commune.") + .WithDiagnostic( + "Les montants ont atteint l’opĂ©ration sans avoir Ă©tĂ© convertis dans une devise commune.", + ErrorOrigin.Internal, + "VĂ©rifiez Ă  quel endroit les montants auraient dĂ» ĂȘtre convertis avant cette opĂ©ration.") .WithExamples(() => CurrencyMismatch( new Amount(10, Currency.EUR), new Amount(12, Currency.USD))); From 8d44bd22aaba45d8f7b6064c595aed7b1036b098 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 21:19:40 +0000 Subject: [PATCH 14/19] docs: align getting started generation with the release build Step 6 builds the solution with -c Release but ran fce generate --no-build without --configuration Release, so the generator resolved Debug assemblies that were never produced and emitted an empty catalog. Pass the configuration explicitly in both language versions and explain why it must match the build. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LUaMS5EHzAdQHBJzrYvKTj --- doc/GettingStarted.en.md | 3 +++ doc/GettingStarted.fr.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/doc/GettingStarted.en.md b/doc/GettingStarted.en.md index fdb8e9d..d578b14 100644 --- a/doc/GettingStarted.en.md +++ b/doc/GettingStarted.en.md @@ -140,12 +140,15 @@ dotnet build MyApp.sln -c Release fce generate \ --solution MyApp.sln \ + --configuration Release \ --no-build \ --format markdown \ --service-name my-api \ --output artifacts/errors.md ``` +`--configuration Release` matches the build above: with `--no-build`, the generator reads the assemblies produced by that exact configuration (it defaults to `Debug` otherwise). + The generated document contains an entry for `AMOUNT_CURRENCY_MISMATCH`, including its description, rule, diagnostic hypotheses, and the messages produced by the example factory. A shortened result looks like this: diff --git a/doc/GettingStarted.fr.md b/doc/GettingStarted.fr.md index 929e5a9..14ca921 100644 --- a/doc/GettingStarted.fr.md +++ b/doc/GettingStarted.fr.md @@ -140,12 +140,15 @@ dotnet build MyApp.sln -c Release fce generate \ --solution MyApp.sln \ + --configuration Release \ --no-build \ --format markdown \ --service-name my-api \ --output artifacts/errors.md ``` +`--configuration Release` correspond au build ci-dessus : avec `--no-build`, le gĂ©nĂ©rateur lit les assemblies produits par cette configuration prĂ©cise (Ă  dĂ©faut, il utilise `Debug`). + Le document gĂ©nĂ©rĂ© contient une entrĂ©e pour `AMOUNT_CURRENCY_MISMATCH`, avec sa description, sa rĂšgle, ses hypothĂšses de diagnostic et les messages produits par l’exemple. Un rĂ©sultat abrĂ©gĂ© ressemble Ă  ceci : From c126fab7bf3e4aa67e3faacf492e1655ae4ce35f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 06:57:41 +0000 Subject: [PATCH 15/19] docs: gloss library terms at first use in the foundation pages A newcomer met several library-specific terms before any page defined them. Give each one a short in-place gloss at its first use, in both languages: - readme: introduce Outcome as an explicit result returned without throwing instead of the bare generic type, relate DomainError to Error, and announce the generated catalog before the code block; - getting started: explain what ProvidesErrorsFor declares, note the assumed Amount/Currency example types, and unpack the three parts of a diagnostic (cause, origin, investigation lead); - design principles: gloss the staged builder; - core concepts: gloss occurrence and inner errors, link ErrorContext at its first mention; - error taxonomy: gloss interaction direction and transience in the intro, and explain PrimaryPortInnerErrors at the first nesting example with a link to the composition rules. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LUaMS5EHzAdQHBJzrYvKTj --- README.md | 6 +++--- doc/CoreConcepts.en.md | 6 +++--- doc/CoreConcepts.fr.md | 6 +++--- doc/DesignPrinciples.en.md | 2 +- doc/DesignPrinciples.fr.md | 2 +- doc/ErrorTaxonomy.en.md | 4 +++- doc/ErrorTaxonomy.fr.md | 4 +++- doc/GettingStarted.en.md | 7 +++++-- doc/GettingStarted.fr.md | 7 +++++-- doc/README.fr.md | 6 +++--- 10 files changed, 30 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index db3bc94..ed5723b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ FirstClassErrors is a .NET library for application errors that need to be understood, diagnosed, documented, and preserved over time. -Instead of scattering error codes and messages throughout the codebase, you define each meaningful error situation once, in a named factory. The same structured `Error` can then be thrown as an exception, carried in an `Outcome`, logged, and included in a generated error catalog. +Instead of scattering error codes and messages throughout the codebase, you define each meaningful error situation once, in a named factory. The same structured `Error` can then be thrown as an exception or returned as an explicit result without throwing (the library's `Outcome` type), logged, and included in a generated error catalog. ## 🚹 The problem @@ -41,7 +41,7 @@ When that knowledge lives in logs, tickets, comments, and people's memories, it ## 💡 The FirstClassErrors approach -A factory gives the error situation a stable identity and keeps its construction in one place: +A factory gives the error situation a stable identity, keeps its construction in one place, and attaches structured documentation that will feed a generated, human-readable catalog: ```csharp [ProvidesErrorsFor(nameof(Amount))] @@ -80,7 +80,7 @@ if (Currency != other.Currency) { } ``` -The factory returns an `Error`, so expected failures can use the same model without throwing: +The factory returns an `Error` (`DomainError` is one of its categories), so expected failures can use the same model without throwing: ```csharp return Outcome.Failure( diff --git a/doc/CoreConcepts.en.md b/doc/CoreConcepts.en.md index 8035f71..92b3300 100644 --- a/doc/CoreConcepts.en.md +++ b/doc/CoreConcepts.en.md @@ -38,14 +38,14 @@ One factory should therefore correspond to one documented error case. ## What an `Error` carries -An error occurrence contains: +An error occurrence — one runtime instance of the situation — contains: - a stable `Code`; - a unique `InstanceId`; - an `OccurredAt` timestamp; - public and internal messages; - optional typed context; -- optional inner errors. +- optional inner errors (nested errors recording the cause). The factory centralizes how these values are created, so every occurrence of the same situation remains consistent. @@ -109,7 +109,7 @@ The runtime error describes one **occurrence**: - the actual diagnostic message; - occurrence-specific context values. -For example, `ORDER_NOT_FOUND` is the stable category. `OrderId = 42` belongs to one occurrence and therefore belongs in `ErrorContext`. +For example, `ORDER_NOT_FOUND` is the stable category. `OrderId = 42` belongs to one occurrence and therefore belongs in [`ErrorContext`](ErrorContext.en.md). ## Diagnostics guide investigation diff --git a/doc/CoreConcepts.fr.md b/doc/CoreConcepts.fr.md index df8b43d..aab4dde 100644 --- a/doc/CoreConcepts.fr.md +++ b/doc/CoreConcepts.fr.md @@ -38,14 +38,14 @@ Une factory doit donc correspondre Ă  un cas d’erreur documentĂ©. ## Ce que porte une `Error` -Une occurrence d’erreur contient : +Une occurrence d’erreur — une instance de la situation Ă  l’exĂ©cution — contient : - un `Code` stable ; - un `InstanceId` unique ; - un horodatage `OccurredAt` ; - des messages publics et internes ; - un contexte typĂ© optionnel ; -- des erreurs internes optionnelles. +- des erreurs internes optionnelles (erreurs imbriquĂ©es qui enregistrent la cause). La factory centralise la crĂ©ation de ces valeurs, afin que chaque occurrence d’une mĂȘme situation reste cohĂ©rente. @@ -109,7 +109,7 @@ L’erreur d’exĂ©cution dĂ©crit une **occurrence** : - son message de diagnostic rĂ©el ; - les valeurs de contexte propres Ă  cette occurrence. -Par exemple, `ORDER_NOT_FOUND` est la catĂ©gorie stable. `OrderId = 42` appartient Ă  une occurrence prĂ©cise et doit donc ĂȘtre placĂ© dans `ErrorContext`. +Par exemple, `ORDER_NOT_FOUND` est la catĂ©gorie stable. `OrderId = 42` appartient Ă  une occurrence prĂ©cise et doit donc ĂȘtre placĂ© dans [`ErrorContext`](ErrorContext.fr.md). ## Les diagnostics orientent l’investigation diff --git a/doc/DesignPrinciples.en.md b/doc/DesignPrinciples.en.md index b0acb10..fedb16e 100644 --- a/doc/DesignPrinciples.en.md +++ b/doc/DesignPrinciples.en.md @@ -44,7 +44,7 @@ FirstClassErrors separates: - public messages intended for users or API clients; - an internal diagnostic message intended for logs, support, and developers. -The staged builder enforces that distinction when the error is created. +The staged builder — each construction step only exposes the next valid calls — enforces that distinction when the error is created. **Consequence:** public messages remain safe and controlled, while diagnostics can still contain the detail required for investigation. diff --git a/doc/DesignPrinciples.fr.md b/doc/DesignPrinciples.fr.md index 467a15a..26a2e96 100644 --- a/doc/DesignPrinciples.fr.md +++ b/doc/DesignPrinciples.fr.md @@ -44,7 +44,7 @@ FirstClassErrors sĂ©pare : - les messages publics destinĂ©s aux utilisateurs ou clients d’API ; - le message de diagnostic interne destinĂ© aux logs, au support et aux dĂ©veloppeurs. -Le builder Ă©tagĂ© impose cette distinction lors de la crĂ©ation de l’erreur. +Le builder Ă©tagĂ© — chaque Ă©tape de construction n’expose que les appels valides suivants — impose cette distinction lors de la crĂ©ation de l’erreur. **ConsĂ©quence :** les messages publics restent sĂ»rs et maĂźtrisĂ©s, tandis que les diagnostics peuvent conserver les dĂ©tails nĂ©cessaires Ă  l’investigation. diff --git a/doc/ErrorTaxonomy.en.md b/doc/ErrorTaxonomy.en.md index d662063..1aa8b90 100644 --- a/doc/ErrorTaxonomy.en.md +++ b/doc/ErrorTaxonomy.en.md @@ -5,7 +5,7 @@ FirstClassErrors distinguishes the **nature of a failure** from the place where it happens to be detected. -That distinction helps domain code remain independent from infrastructure while giving operational tooling useful signals such as interaction direction and transience. +That distinction helps domain code remain independent from infrastructure while giving operational tooling useful signals such as interaction direction (incoming or outgoing) and transience (whether retrying may succeed). ## The hierarchy at a glance @@ -63,6 +63,8 @@ return PrimaryPortError.Create( shortMessage: "The request contains invalid data."); ``` +`PrimaryPortInnerErrors` is a typed collection that only accepts the error types a primary-port error may nest (see [Composition rules](#composition-rules)). + The domain error still describes the violated business rule. The primary-port error describes the boundary condition: this incoming interaction is rejected. When inner errors are supplied, the port error computes its overall transience from them. ## `SecondaryPortError`: an outgoing interaction failed diff --git a/doc/ErrorTaxonomy.fr.md b/doc/ErrorTaxonomy.fr.md index da6a451..0001a41 100644 --- a/doc/ErrorTaxonomy.fr.md +++ b/doc/ErrorTaxonomy.fr.md @@ -5,7 +5,7 @@ FirstClassErrors distingue la **nature d’un Ă©chec** de l’endroit oĂč il est dĂ©tectĂ©. -Cette distinction permet au domaine de rester indĂ©pendant de l’infrastructure, tout en fournissant Ă  l’exploitation des signaux utiles comme la direction de l’interaction et la transience. +Cette distinction permet au domaine de rester indĂ©pendant de l’infrastructure, tout en fournissant Ă  l’exploitation des signaux utiles comme la direction de l’interaction (entrante ou sortante) et la transience (retenter peut-il rĂ©ussir ?). ## La hiĂ©rarchie en un coup d’Ɠil @@ -63,6 +63,8 @@ return PrimaryPortError.Create( shortMessage: "La requĂȘte contient des donnĂ©es invalides."); ``` +`PrimaryPortInnerErrors` est une collection typĂ©e qui n’accepte que les types d’erreurs qu’une erreur de port primaire peut imbriquer (voir [RĂšgles de composition](#rĂšgles-de-composition)). + L’erreur de domaine dĂ©crit toujours la rĂšgle violĂ©e. L’erreur de port primaire dĂ©crit la condition Ă  la frontiĂšre : cette interaction entrante est rejetĂ©e. Lorsque des erreurs internes sont fournies, l’erreur de port calcule sa transience globale Ă  partir d’elles. ## `SecondaryPortError` : une interaction sortante a Ă©chouĂ© diff --git a/doc/GettingStarted.en.md b/doc/GettingStarted.en.md index d578b14..10cf3d4 100644 --- a/doc/GettingStarted.en.md +++ b/doc/GettingStarted.en.md @@ -41,7 +41,7 @@ The marker must be present in the `.csproj` itself. Projects without it are skip ## 3. Define one error situation -Create a static factory class. Each factory method represents one precise situation the system recognizes. +Create a static factory class. Each factory method represents one precise situation the system recognizes. The examples use a small `Amount` value type with a `Currency`; substitute any type from your own domain. ```csharp using FirstClassErrors; @@ -66,8 +66,9 @@ public static class InvalidAmountOperationError { } ``` -The three important parts are: +The four important parts are: +- `[ProvidesErrorsFor]` ties the factory class to the concept whose errors it declares; the generator uses it to find and group errors; - the factory name expresses the situation in code; - the error code is its stable, machine-readable identity; - the diagnostic message is internal, while the public messages are safe for callers. @@ -99,6 +100,8 @@ private static ErrorDocumentation CurrencyMismatchDocumentation() { This is structured knowledge rather than a comment: the generator can extract the title, explanation, rule, diagnostics, and real example produced by the factory. +Each diagnostic is a hypothesis: a plausible cause, an origin (whether the suspected cause lies inside the system or with an external caller), and an investigation lead. + ## 5. Use the error When the failure is exceptional, turn the error into its paired exception: diff --git a/doc/GettingStarted.fr.md b/doc/GettingStarted.fr.md index 14ca921..71ee218 100644 --- a/doc/GettingStarted.fr.md +++ b/doc/GettingStarted.fr.md @@ -41,7 +41,7 @@ Le marqueur doit ĂȘtre prĂ©sent dans le `.csproj` lui-mĂȘme. Les projets qui ne ## 3. DĂ©finir une situation d’erreur -CrĂ©ez une classe factory statique. Chaque mĂ©thode factory reprĂ©sente une situation prĂ©cise reconnue par le systĂšme. +CrĂ©ez une classe factory statique. Chaque mĂ©thode factory reprĂ©sente une situation prĂ©cise reconnue par le systĂšme. Les exemples utilisent un petit type valeur `Amount` dotĂ© d’une `Currency` ; remplacez-le par n’importe quel type de votre propre domaine. ```csharp using FirstClassErrors; @@ -66,8 +66,9 @@ public static class InvalidAmountOperationError { } ``` -Trois Ă©lĂ©ments sont essentiels : +Quatre Ă©lĂ©ments sont essentiels : +- `[ProvidesErrorsFor]` relie la classe factory au concept dont elle dĂ©clare les erreurs ; le gĂ©nĂ©rateur s’en sert pour les dĂ©couvrir et les regrouper ; - le nom de la factory exprime la situation dans le code ; - le code d’erreur constitue son identitĂ© stable et lisible par machine ; - le message de diagnostic est interne, tandis que les messages publics sont sĂ»rs pour les appelants. @@ -99,6 +100,8 @@ private static ErrorDocumentation CurrencyMismatchDocumentation() { Il s’agit de connaissance structurĂ©e, pas d’un commentaire : le gĂ©nĂ©rateur peut extraire le titre, l’explication, la rĂšgle, les hypothĂšses de diagnostic et l’exemple rĂ©ellement produit par la factory. +Chaque diagnostic est une hypothĂšse : une cause plausible, une origine (le soupçon porte-t-il sur l’intĂ©rieur du systĂšme ou sur un appelant externe ?) et une piste d’investigation. + ## 5. Utiliser l’erreur Lorsque l’échec est exceptionnel, transformez l’erreur en son exception associĂ©e : diff --git a/doc/README.fr.md b/doc/README.fr.md index 0682b5b..64dded3 100644 --- a/doc/README.fr.md +++ b/doc/README.fr.md @@ -19,7 +19,7 @@ FirstClassErrors est une bibliothĂšque .NET destinĂ©e aux erreurs applicatives qui doivent ĂȘtre comprises, diagnostiquĂ©es, documentĂ©es et conservĂ©es dans le temps. -Au lieu de disperser les codes et messages d’erreur dans le code, vous dĂ©finissez chaque situation significative une seule fois, dans une factory nommĂ©e. La mĂȘme `Error` structurĂ©e peut ensuite ĂȘtre levĂ©e sous forme d’exception, transportĂ©e dans un `Outcome`, journalisĂ©e et intĂ©grĂ©e Ă  un catalogue d’erreurs gĂ©nĂ©rĂ© automatiquement. +Au lieu de disperser les codes et messages d’erreur dans le code, vous dĂ©finissez chaque situation significative une seule fois, dans une factory nommĂ©e. La mĂȘme `Error` structurĂ©e peut ensuite ĂȘtre levĂ©e sous forme d’exception ou renvoyĂ©e comme rĂ©sultat explicite sans exception (le type `Outcome` de la bibliothĂšque), journalisĂ©e et intĂ©grĂ©e Ă  un catalogue d’erreurs gĂ©nĂ©rĂ© automatiquement. ## 🚹 Le problĂšme @@ -41,7 +41,7 @@ Lorsque cette connaissance est rĂ©partie entre les logs, les tickets, les commen ## 💡 L’approche FirstClassErrors -Une factory donne une identitĂ© stable Ă  la situation d’erreur et centralise sa construction : +Une factory donne une identitĂ© stable Ă  la situation d’erreur, centralise sa construction et y attache une documentation structurĂ©e qui alimentera un catalogue gĂ©nĂ©rĂ©, lisible par des humains : ```csharp [ProvidesErrorsFor(nameof(Amount))] @@ -80,7 +80,7 @@ if (Currency != other.Currency) { } ``` -La factory retourne une `Error`, donc un Ă©chec attendu peut employer le mĂȘme modĂšle sans lever d’exception : +La factory retourne une `Error` (`DomainError` en est une des catĂ©gories), donc un Ă©chec attendu peut employer le mĂȘme modĂšle sans lever d’exception : ```csharp return Outcome.Failure( From 653ec22e130a8054815ccd5327db8752b3f8c76a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 08:48:49 +0000 Subject: [PATCH 16/19] docs: rewrite the readme intro around one unifying idea The intro led with truisms (errors should be understood, diagnosed...) and buried the actual differentiator at the end of a sentence, using the bare Outcome type with no gloss. Replace it with a direct claim (errors as a first-class domain concept, described once, reused everywhere) and break the five things the library does into short, titled blocks instead of a dense paragraph: modeling the error, handling the failure, documenting it, checking the model at compile time, and the upcoming external-input binder. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LUaMS5EHzAdQHBJzrYvKTj --- README.md | 19 +++++++++++++++++-- doc/README.fr.md | 19 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ed5723b..b556eff 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,24 @@ ![FirstClassErrors](./doc/images/first-class-errors.png "FirstClassErrors") -FirstClassErrors is a .NET library for application errors that need to be understood, diagnosed, documented, and preserved over time. +An application error too often boils down to a bare code or string, with nothing that explains what it actually means. FirstClassErrors turns it into a first-class concept of your domain. You describe each error situation once, with its rules, in one place in the code; the whole library then builds on that model. -Instead of scattering error codes and messages throughout the codebase, you define each meaningful error situation once, in a named factory. The same structured `Error` can then be thrown as an exception or returned as an explicit result without throwing (the library's `Outcome` type), logged, and included in a generated error catalog. +From there, it covers the full chain: + +**Model the error** +An error becomes an object that carries its own meaning — a violated business rule, a rejected incoming request, or a failing external service, transient or not. + +**Handle the failure** +You choose how it travels: thrown as an ordinary exception, or returned as an explicit result the caller inspects without a try/catch. + +**Document the error** +Each error can carry its explanation, its rule, and its likely causes right there in the code. FirstClassErrors generates a human-readable catalog from that same code — so it never drifts. + +**Check the model** +Roslyn analyzers enforce these rules at compile time: a duplicated error code or inconsistent documentation becomes a build error, caught before it ever reaches production. + +**Bind external input** +A form or a JSON request must become a valid domain object. Where classic .NET binding restates those rules separately, in a validator or attributes, FirstClassErrors binds that input directly to your value objects, reusing their own construction rules instead of duplicating them. ## 🚹 The problem diff --git a/doc/README.fr.md b/doc/README.fr.md index 64dded3..1b7f5bc 100644 --- a/doc/README.fr.md +++ b/doc/README.fr.md @@ -17,9 +17,24 @@ ![FirstClassErrors](./images/first-class-errors.png "FirstClassErrors") -FirstClassErrors est une bibliothĂšque .NET destinĂ©e aux erreurs applicatives qui doivent ĂȘtre comprises, diagnostiquĂ©es, documentĂ©es et conservĂ©es dans le temps. +Une erreur applicative se rĂ©duit trop souvent Ă  un code ou une chaĂźne de caractĂšres, sans rien qui en explique le sens. FirstClassErrors en fait un concept de premiĂšre classe de votre domaine. Vous dĂ©crivez chaque situation d’erreur une seule fois, avec ses rĂšgles, Ă  un seul endroit du code ; toute la bibliothĂšque s’appuie ensuite sur ce modĂšle. -Au lieu de disperser les codes et messages d’erreur dans le code, vous dĂ©finissez chaque situation significative une seule fois, dans une factory nommĂ©e. La mĂȘme `Error` structurĂ©e peut ensuite ĂȘtre levĂ©e sous forme d’exception ou renvoyĂ©e comme rĂ©sultat explicite sans exception (le type `Outcome` de la bibliothĂšque), journalisĂ©e et intĂ©grĂ©e Ă  un catalogue d’erreurs gĂ©nĂ©rĂ© automatiquement. +À partir de lĂ , elle couvre toute la chaĂźne : + +**ModĂ©liser l’erreur** +Une erreur devient un objet qui porte son propre sens — qu’il s’agisse d’une rĂšgle mĂ©tier violĂ©e, d’un appel entrant rejetĂ© ou d’une panne d’un service externe, passagĂšre ou dĂ©finitive. + +**GĂ©rer l’échec** +À vous de choisir comment elle voyage : levĂ©e comme une exception classique, ou renvoyĂ©e comme un rĂ©sultat explicite que l’appelant inspecte sans try/catch. + +**Documenter l’erreur** +Chaque erreur peut porter, dans le code, son explication, sa rĂšgle et ses causes probables. FirstClassErrors en gĂ©nĂšre un catalogue lisible, extrait de ce mĂȘme code — il ne dĂ©rive donc jamais. + +**VĂ©rifier le modĂšle** +Des analyseurs Roslyn contrĂŽlent ces rĂšgles dĂšs la compilation : un code d’erreur dupliquĂ© ou une documentation incohĂ©rente deviennent des erreurs de build, dĂ©tectĂ©es avant la production. + +**Lier les entrĂ©es externes** +Un formulaire ou une requĂȘte JSON doit devenir un objet mĂ©tier valide. LĂ  oĂč le binding .NET classique redĂ©clare ces rĂšgles sĂ©parĂ©ment, dans un validateur ou des attributs, FirstClassErrors lie ces entrĂ©es directement Ă  vos objets valeur, en rĂ©utilisant leurs propres rĂšgles de construction au lieu de les dupliquer. ## 🚹 Le problĂšme From 7369209b6b4d7cc64543ca33fc7e7d9627d7f4f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 08:56:18 +0000 Subject: [PATCH 17/19] docs: give the readme intro its own heading Add a '## One model for the whole chain' heading so the intro reads as a proper section like the rest of the readme, instead of orphan prose under the tagline. Drop the inline transition sentence the heading now makes redundant. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LUaMS5EHzAdQHBJzrYvKTj --- README.md | 4 ++-- doc/README.fr.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b556eff..b075b80 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ ![FirstClassErrors](./doc/images/first-class-errors.png "FirstClassErrors") -An application error too often boils down to a bare code or string, with nothing that explains what it actually means. FirstClassErrors turns it into a first-class concept of your domain. You describe each error situation once, with its rules, in one place in the code; the whole library then builds on that model. +## đŸ§© One model for the whole chain -From there, it covers the full chain: +An application error too often boils down to a bare code or string, with nothing that explains what it actually means. FirstClassErrors turns it into a first-class concept of your domain. You describe each error situation once, with its rules, in one place in the code; the whole library then builds on that model. **Model the error** An error becomes an object that carries its own meaning — a violated business rule, a rejected incoming request, or a failing external service, transient or not. diff --git a/doc/README.fr.md b/doc/README.fr.md index 1b7f5bc..1903a79 100644 --- a/doc/README.fr.md +++ b/doc/README.fr.md @@ -17,9 +17,9 @@ ![FirstClassErrors](./images/first-class-errors.png "FirstClassErrors") -Une erreur applicative se rĂ©duit trop souvent Ă  un code ou une chaĂźne de caractĂšres, sans rien qui en explique le sens. FirstClassErrors en fait un concept de premiĂšre classe de votre domaine. Vous dĂ©crivez chaque situation d’erreur une seule fois, avec ses rĂšgles, Ă  un seul endroit du code ; toute la bibliothĂšque s’appuie ensuite sur ce modĂšle. +## đŸ§© Un seul modĂšle pour toute la chaĂźne -À partir de lĂ , elle couvre toute la chaĂźne : +Une erreur applicative se rĂ©duit trop souvent Ă  un code ou une chaĂźne de caractĂšres, sans rien qui en explique le sens. FirstClassErrors en fait un concept de premiĂšre classe de votre domaine. Vous dĂ©crivez chaque situation d’erreur une seule fois, avec ses rĂšgles, Ă  un seul endroit du code ; toute la bibliothĂšque s’appuie ensuite sur ce modĂšle. **ModĂ©liser l’erreur** Une erreur devient un objet qui porte son propre sens — qu’il s’agisse d’une rĂšgle mĂ©tier violĂ©e, d’un appel entrant rejetĂ© ou d’une panne d’un service externe, passagĂšre ou dĂ©finitive. From c6a1429d70f180550bca74cfc56b183dd1f14d4a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 09:41:18 +0000 Subject: [PATCH 18/19] docs: fix the PrimaryPortError example to compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example called InvalidAmountError.NegativeValue(...) and referenced Code.RequestRejected, neither of which exists anywhere in the codebase — invented pseudo-code that cannot compile. Replace the leading illustration with the real DateOutOfStatementPeriod primary-port error from FirstClassErrors.Usage (a genuine file-import rejection), and rebuild the composition example from real API members: the recurring InvalidAmountOperationError.CurrencyMismatch domain error nested via PrimaryPortInnerErrors, instead of the fabricated method and code. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LUaMS5EHzAdQHBJzrYvKTj --- doc/ErrorTaxonomy.en.md | 31 ++++++++++++++++++++----------- doc/ErrorTaxonomy.fr.md | 31 ++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/doc/ErrorTaxonomy.en.md b/doc/ErrorTaxonomy.en.md index 1aa8b90..6cc3a66 100644 --- a/doc/ErrorTaxonomy.en.md +++ b/doc/ErrorTaxonomy.en.md @@ -44,23 +44,32 @@ The type follows **which rule was violated**, not which layer happened to observ A primary port represents an interaction entering the application: HTTP, messaging, a CLI command, a file import, or another inbound adapter. -Consider an API request containing an invalid amount. Two facts may need to be preserved: +Consider a bank-statement file import rejecting a transaction whose date falls outside the statement period: -1. the domain rejected the value because an invariant was violated; -2. the incoming request therefore cannot be accepted. +```csharp +internal static PrimaryPortError DateOutOfStatementPeriod(DateOnly periodStart, DateOnly periodEnd, DateOnly transactionDate) { + return PrimaryPortError.Create( + Code.DateOutOfStatementPeriod, + $"Transaction dated {transactionDate} is outside the statement period [{periodStart};{periodEnd}].", + Transience.NonTransient, + ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) + .WithPublicMessage("The file contains a transaction outside the statement period."); +} +``` -The adapter can wrap the domain cause in a primary-port error: +The port error describes the boundary condition on its own: this incoming file cannot be accepted as it stands. + +Two facts may instead need to be preserved together: the domain rejected a value because an invariant was violated, and the incoming request therefore cannot be accepted. The adapter can wrap the domain cause in a primary-port error: ```csharp -DomainError invalidAmount = InvalidAmountError.NegativeValue(request.Amount); -var innerErrors = new PrimaryPortInnerErrors().Add(invalidAmount); +DomainError mismatch = InvalidAmountOperationError.CurrencyMismatch(left, right); +var innerErrors = new PrimaryPortInnerErrors().Add(mismatch); return PrimaryPortError.Create( - Code.RequestRejected, - diagnosticMessage: $"Request {request.Id} contains an invalid amount.", - innerErrors: innerErrors) - .WithPublicMessage( - shortMessage: "The request contains invalid data."); + ErrorCode.Create("REQUEST_REJECTED"), + $"Request rejected: {mismatch.DiagnosticMessage}", + innerErrors) + .WithPublicMessage("The request contains invalid data."); ``` `PrimaryPortInnerErrors` is a typed collection that only accepts the error types a primary-port error may nest (see [Composition rules](#composition-rules)). diff --git a/doc/ErrorTaxonomy.fr.md b/doc/ErrorTaxonomy.fr.md index 0001a41..82d1b42 100644 --- a/doc/ErrorTaxonomy.fr.md +++ b/doc/ErrorTaxonomy.fr.md @@ -44,23 +44,32 @@ Le type dĂ©pend de **la rĂšgle violĂ©e**, pas de la couche qui constate l’éch Un port primaire reprĂ©sente une interaction qui entre dans l’application : HTTP, message, commande CLI, import de fichier ou autre adapter entrant. -Prenons une requĂȘte API contenant un montant invalide. Deux faits peuvent devoir ĂȘtre conservĂ©s : +Prenons l’import d’un fichier de relevĂ© bancaire qui rejette une transaction dont la date tombe hors de la pĂ©riode du relevĂ© : -1. le domaine a refusĂ© la valeur parce qu’un invariant a Ă©tĂ© violĂ© ; -2. la requĂȘte entrante ne peut donc pas ĂȘtre acceptĂ©e. +```csharp +internal static PrimaryPortError DateOutOfStatementPeriod(DateOnly periodStart, DateOnly periodEnd, DateOnly transactionDate) { + return PrimaryPortError.Create( + Code.DateOutOfStatementPeriod, + $"La transaction datĂ©e du {transactionDate} est hors de la pĂ©riode de relevĂ© [{periodStart};{periodEnd}].", + Transience.NonTransient, + ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) + .WithPublicMessage("Le fichier contient une transaction hors de la pĂ©riode de relevĂ©."); +} +``` -L’adapter peut encapsuler la cause mĂ©tier dans une erreur de port primaire : +L’erreur de port dĂ©crit Ă  elle seule la condition Ă  la frontiĂšre : ce fichier entrant ne peut pas ĂȘtre acceptĂ© tel quel. + +Deux faits peuvent au contraire devoir ĂȘtre conservĂ©s ensemble : le domaine a refusĂ© une valeur parce qu’un invariant a Ă©tĂ© violĂ©, et la requĂȘte entrante ne peut donc pas ĂȘtre acceptĂ©e. L’adapter peut encapsuler la cause mĂ©tier dans une erreur de port primaire : ```csharp -DomainError invalidAmount = InvalidAmountError.NegativeValue(request.Amount); -var innerErrors = new PrimaryPortInnerErrors().Add(invalidAmount); +DomainError mismatch = InvalidAmountOperationError.CurrencyMismatch(left, right); +var innerErrors = new PrimaryPortInnerErrors().Add(mismatch); return PrimaryPortError.Create( - Code.RequestRejected, - diagnosticMessage: $"La requĂȘte {request.Id} contient un montant invalide.", - innerErrors: innerErrors) - .WithPublicMessage( - shortMessage: "La requĂȘte contient des donnĂ©es invalides."); + ErrorCode.Create("REQUEST_REJECTED"), + $"RequĂȘte rejetĂ©e : {mismatch.DiagnosticMessage}", + innerErrors) + .WithPublicMessage("La requĂȘte contient des donnĂ©es invalides."); ``` `PrimaryPortInnerErrors` est une collection typĂ©e qui n’accepte que les types d’erreurs qu’une erreur de port primaire peut imbriquer (voir [RĂšgles de composition](#rĂšgles-de-composition)). From 8cddfb8950c7a6d0d22e0a1ba8f19018940038f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 11:06:41 +0000 Subject: [PATCH 19/19] docs: reframe why diagnostics stay hypotheses The opening sentence read as discouraging (root cause unknown, so...) without saying why documenting causes is still worthwhile. State the positive reason instead: several plausible causes can usually be named even without certainty, and naming them gives investigation a starting point. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LUaMS5EHzAdQHBJzrYvKTj --- doc/DesignPrinciples.en.md | 2 +- doc/DesignPrinciples.fr.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/DesignPrinciples.en.md b/doc/DesignPrinciples.en.md index fedb16e..31b5ef4 100644 --- a/doc/DesignPrinciples.en.md +++ b/doc/DesignPrinciples.en.md @@ -63,7 +63,7 @@ The documentation method describes the situation, rule, diagnostic hypotheses, a ## 5. Diagnostics are hypotheses, not verdicts -At the moment an error is defined, its exact root cause is often unknown. Useful documentation should therefore propose plausible explanations and investigation leads without assigning blame. +No one can be certain of an error's exact origin — even a likely cause may itself just be a symptom of something deeper. What can usually be named are several plausible causes, and naming them gives investigation somewhere to start. Prefer: diff --git a/doc/DesignPrinciples.fr.md b/doc/DesignPrinciples.fr.md index 26a2e96..2d37ace 100644 --- a/doc/DesignPrinciples.fr.md +++ b/doc/DesignPrinciples.fr.md @@ -63,7 +63,7 @@ La mĂ©thode de documentation dĂ©crit la situation, la rĂšgle, les hypothĂšses de ## 5. Les diagnostics sont des hypothĂšses, pas des verdicts -Au moment oĂč une erreur est dĂ©finie, sa cause racine exacte est souvent inconnue. Une documentation utile doit donc proposer des explications plausibles et des pistes d’investigation sans attribuer de faute. +Personne ne peut ĂȘtre certain de l’origine exacte d’une erreur — mĂȘme une cause probable peut n’ĂȘtre que le symptĂŽme d’une cause plus profonde. Ce qui peut gĂ©nĂ©ralement ĂȘtre identifiĂ©, ce sont plusieurs causes plausibles, et les nommer donne Ă  l’investigation un point de dĂ©part. PrĂ©fĂ©rez :