Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions FirstClassErrors.Usage/Model/Amount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ public Amount(decimal value, Currency currency) {
public decimal Value { get; }
public Currency Currency { get; }

public Amount Add(Amount other) {
public Amount AddOrThrow(Amount other) {
ArgumentNullException.ThrowIfNull(other);

EnsureSameCurrency(other);

return new Amount(Value + other.Value, Currency);
}

public Amount Subtract(Amount other) {
public Amount SubtractOrThrow(Amount other) {
ArgumentNullException.ThrowIfNull(other);

EnsureSameCurrency(other);
Expand Down
18 changes: 9 additions & 9 deletions FirstClassErrors.Usage/Model/Temperature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed class Temperature : IEquatable<Temperature>, IComparable<Temperatu
/// <summary>
/// Represents the absolute zero temperature, the lowest possible temperature where particles have minimal energy.
/// </summary>
public static readonly Temperature AbsoluteZero = FromKelvin(AbsoluteZeroInKelvin);
public static readonly Temperature AbsoluteZero = FromKelvinOrThrow(AbsoluteZeroInKelvin);

/// <summary>
/// Creates a <see cref="Temperature" /> from a kelvin value.
Expand All @@ -35,20 +35,20 @@ public sealed class Temperature : IEquatable<Temperature>, IComparable<Temperatu
/// <exception cref="DomainException">
/// Thrown when <paramref name="kelvin" /> is lower than absolute zero.
/// </exception>
public static Temperature FromKelvin(decimal kelvin) {
return TryFromKelvin(kelvin).GetResultOrThrow();
public static Temperature FromKelvinOrThrow(decimal kelvin) {
return FromKelvin(kelvin).GetResultOrThrow();
}

/// <summary>
/// Attempts to create a <see cref="Temperature" /> from a kelvin value.
/// Creates a <see cref="Temperature" /> from a kelvin value.
/// </summary>
/// <param name="kelvin">Temperature in kelvin.</param>
/// <returns>
/// A <c>Outcome&lt;Temperature&gt;</c> that is successful when <paramref name="kelvin" />
/// is not below absolute zero; otherwise a failure containing the corresponding
/// <see cref="InvalidTemperatureError" />.
/// </returns>
public static Outcome<Temperature> TryFromKelvin(decimal kelvin) {
public static Outcome<Temperature> FromKelvin(decimal kelvin) {
if (IsLowerThanAbsoluteZero(kelvin)) { return Outcome<Temperature>.Failure(InvalidTemperatureError.BelowAbsoluteZero(kelvin, TemperatureUnit.Kelvin)); }

return Outcome<Temperature>.Success(new Temperature(kelvin));
Expand All @@ -62,20 +62,20 @@ public static Outcome<Temperature> TryFromKelvin(decimal kelvin) {
/// <exception cref="DomainException">
/// Thrown when the converted kelvin value is lower than absolute zero.
/// </exception>
public static Temperature FromCelsius(decimal celsius) {
return TryFromCelsius(celsius).GetResultOrThrow();
public static Temperature FromCelsiusOrThrow(decimal celsius) {
return FromCelsius(celsius).GetResultOrThrow();
}

/// <summary>
/// Attempts to create a <see cref="Temperature" /> from a Celsius value.
/// Creates a <see cref="Temperature" /> from a Celsius value.
/// </summary>
/// <param name="celsius">Temperature in degrees Celsius.</param>
/// <returns>
/// A <c>Outcome&lt;Temperature&gt;</c> that is successful when the Celsius value
/// is not below absolute zero; otherwise a failure containing the corresponding
/// <see cref="InvalidTemperatureError" />.
/// </returns>
public static Outcome<Temperature> TryFromCelsius(decimal celsius) {
public static Outcome<Temperature> FromCelsius(decimal celsius) {
decimal kelvin = celsius + CelsiusToKelvinOffset;
if (IsLowerThanAbsoluteZero(kelvin)) { return Outcome<Temperature>.Failure(InvalidTemperatureError.BelowAbsoluteZero(celsius, TemperatureUnit.Celsius)); }

Expand Down
4 changes: 2 additions & 2 deletions doc/GettingStarted.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Each diagnostic is a hypothesis: a plausible cause, an origin (whether the suspe
When the failure is exceptional, turn the error into its paired exception:

```csharp
public Amount Add(Amount other) {
public Amount AddOrThrow(Amount other) {
if (Currency != other.Currency) {
throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException();
}
Expand All @@ -121,7 +121,7 @@ The business code names the situation without repeating codes or messages.
When failure is an expected part of the flow, carry the same `Error` without throwing:

```csharp
public static Outcome<Amount> TryAdd(Amount left, Amount right) {
public static Outcome<Amount> Add(Amount left, Amount right) {
if (left.Currency != right.Currency) {
return Outcome<Amount>.Failure(
InvalidAmountOperationError.CurrencyMismatch(left, right));
Expand Down
4 changes: 2 additions & 2 deletions doc/GettingStarted.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Chaque diagnostic est une hypothèse : une cause plausible, une origine (le soup
Lorsque l’échec est exceptionnel, transformez l’erreur en son exception associée :

```csharp
public Amount Add(Amount other) {
public Amount AddOrThrow(Amount other) {
if (Currency != other.Currency) {
throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException();
}
Expand All @@ -121,7 +121,7 @@ Le code métier nomme la situation sans répéter les codes ni les messages.
Lorsque l’échec fait normalement partie du flux, transportez la même `Error` sans la lever :

```csharp
public static Outcome<Amount> TryAdd(Amount left, Amount right) {
public static Outcome<Amount> Add(Amount left, Amount right) {
if (left.Currency != right.Currency) {
return Outcome<Amount>.Failure(
InvalidAmountOperationError.CurrencyMismatch(left, right));
Expand Down
14 changes: 7 additions & 7 deletions doc/OutcomeGuide.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ return Outcome<Amount>.Failure(
Use `IsSuccess` and `IsFailure` when explicit branching is clearest.

```csharp
Outcome<Amount> result = TryCreateAmount(value, currencyCode);
Outcome<Amount> result = CreateAmount(value, currencyCode);

if (result.IsFailure) {
Log(result.Error!);
Expand All @@ -83,15 +83,15 @@ A function that **returns a value** transforms the carried value (a step that ca

```csharp
Outcome<Money> total =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Then(amount => amount.WithVat());
```

A function that **returns an `Outcome`** runs another step that may itself fail:

```csharp
Outcome<Receipt> result =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Then(CheckLimits)
.Then(Charge);
```
Expand All @@ -116,7 +116,7 @@ A value fallback can be returned as a successful outcome:

```csharp
Outcome<Amount> amount =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Recover(error => Outcome<Amount>.Success(Amount.Zero));
```

Expand Down Expand Up @@ -160,7 +160,7 @@ It does nothing on success and throws `error.ToException()` on failure.
Use it with `Outcome<T>` when the boundary requires a value or an exception.

```csharp
Amount amount = TryCreateAmount(value, currencyCode).GetResultOrThrow();
Amount amount = CreateAmount(value, currencyCode).GetResultOrThrow();
```

The exception is created and thrown at this point. The stack trace therefore begins at the escalation point, not where the `Outcome` failure was created.
Expand Down Expand Up @@ -212,13 +212,13 @@ public async Task<Outcome<Receipt>> CheckoutAsync(
string currencyCode,
CancellationToken cancellationToken) {

return await TryCreateAmount(value, currencyCode)
return await CreateAmount(value, currencyCode)
.Then(CheckLimits)
.Then(
(amount, ct) => ChargeAsync(amount, ct),
cancellationToken)
.Recover(
(error, ct) => TryAlternativeProviderAsync(error, ct),
(error, ct) => AlternativeProviderAsync(error, ct),
cancellationToken);
}
```
Expand Down
14 changes: 7 additions & 7 deletions doc/OutcomeGuide.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ return Outcome<Amount>.Failure(
Utilisez `IsSuccess` et `IsFailure` lorsqu’un branchement explicite est le plus lisible.

```csharp
Outcome<Amount> result = TryCreateAmount(value, currencyCode);
Outcome<Amount> result = CreateAmount(value, currencyCode);

if (result.IsFailure) {
Log(result.Error!);
Expand All @@ -83,15 +83,15 @@ Une fonction qui **renvoie une valeur** transforme la valeur portée (une étape

```csharp
Outcome<Money> total =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Then(amount => amount.WithVat());
```

Une fonction qui **renvoie un `Outcome`** exécute une autre étape susceptible d’échouer :

```csharp
Outcome<Receipt> result =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Then(CheckLimits)
.Then(Charge);
```
Expand All @@ -116,7 +116,7 @@ Une valeur de repli peut être retournée sous forme de succès :

```csharp
Outcome<Amount> amount =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Recover(error => Outcome<Amount>.Success(Amount.Zero));
```

Expand Down Expand Up @@ -160,7 +160,7 @@ Elle ne fait rien en cas de succès et lève `error.ToException()` en cas d’é
Utilisez-la avec `Outcome<T>` lorsqu’une frontière exige une valeur ou une exception.

```csharp
Amount amount = TryCreateAmount(value, currencyCode).GetResultOrThrow();
Amount amount = CreateAmount(value, currencyCode).GetResultOrThrow();
```

L’exception est créée et levée à cet endroit. La stack trace commence donc au point d’escalade, pas là où l’échec `Outcome` a été créé.
Expand Down Expand Up @@ -212,13 +212,13 @@ public async Task<Outcome<Receipt>> CheckoutAsync(
string currencyCode,
CancellationToken cancellationToken) {

return await TryCreateAmount(value, currencyCode)
return await CreateAmount(value, currencyCode)
.Then(CheckLimits)
.Then(
(amount, ct) => ChargeAsync(amount, ct),
cancellationToken)
.Recover(
(error, ct) => TryAlternativeProviderAsync(error, ct),
(error, ct) => AlternativeProviderAsync(error, ct),
cancellationToken);
}
```
Expand Down
20 changes: 10 additions & 10 deletions doc/UsagePatterns.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,29 @@ A value object must not enter an invalid state. Whether failure belongs to the c

```csharp
// Contract: returns a valid Temperature or fails — failure has no place in this method's return type.
public static Temperature FromKelvin(decimal kelvin) {
return TryFromKelvin(kelvin).GetResultOrThrow();
public static Temperature FromKelvinOrThrow(decimal kelvin) {
return FromKelvin(kelvin).GetResultOrThrow();
}
```

```csharp
// Contract: an out-of-range value is part of the normal contract — the caller must handle it explicitly.
public static Outcome<Temperature> TryFromKelvin(decimal kelvin) {
public static Outcome<Temperature> FromKelvin(decimal kelvin) {
if (kelvin < 0) { return Outcome<Temperature>.Failure(InvalidTemperatureError.BelowAbsoluteZero(kelvin, TemperatureUnit.Kelvin)); }

return Outcome<Temperature>.Success(new Temperature(kelvin));
}
```

Both report the exact same documented error, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvin` does not repeat the check; it escalates `TryFromKelvin`'s failure with `GetResultOrThrow()`. When both contracts are genuinely useful, centralize validation in the `Outcome`-returning version and derive the throwing one from it — not the reverse.
Both report the exact same documented error, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvinOrThrow` does not repeat the check; it escalates `FromKelvin`'s failure with `GetResultOrThrow()`. When both contracts are genuinely useful, centralize validation in the `Outcome`-returning version and derive the throwing one from it — not the reverse.

## 🧮 Domain operations

Operations between valid domain objects may still violate a rule. The choice here is a matter of API contract, not of who is theoretically allowed to react — a caller of `Add` could still catch and handle a thrown mismatch, just as a caller of `TryAdd` could still ignore a returned one. What differs is what each method promises to its caller:
Operations between valid domain objects may still violate a rule. The choice here is a matter of API contract, not of who is theoretically allowed to react — a caller of `AddOrThrow` could still catch and handle a thrown mismatch, just as a caller of `Add` could still ignore a returned one. What differs is what each method promises to its caller:

```csharp
// Contract: the amounts passed in must already share the same currency.
public Amount Add(Amount other) {
public Amount AddOrThrow(Amount other) {
if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); }

return new Amount(Value + other.Value, Currency);
Expand All @@ -57,7 +57,7 @@ public Amount Add(Amount other) {

```csharp
// Contract: a currency mismatch is an expected outcome the caller must handle.
public Outcome<Amount> TryAdd(Amount other) {
public Outcome<Amount> Add(Amount other) {
if (Currency != other.Currency) { return Outcome<Amount>.Failure(InvalidAmountOperationError.CurrencyMismatch(this, other)); }

return Outcome<Amount>.Success(new Amount(Value + other.Value, Currency));
Expand All @@ -73,7 +73,7 @@ Each item in a batch can fail independently — but whether that failure is part
```csharp
// Contract: a per-line failure is expected and handled locally — log it and move on.
foreach (string line in file) {
TryParseAmount(line).Finally(
ParseAmount(line).Finally(
onSuccess: Process,
onFailure: Log);
}
Expand All @@ -82,7 +82,7 @@ foreach (string line in file) {
```csharp
// Contract: any invalid line invalidates the whole file — stop immediately.
foreach (string line in file) {
Amount amount = TryParseAmount(line).GetResultOrThrow();
Amount amount = ParseAmount(line).GetResultOrThrow();
Process(amount);
}
```
Expand Down Expand Up @@ -148,7 +148,7 @@ When several expected-failure operations must run in sequence, compose their out

```csharp
Outcome<Receipt> result =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Then(CheckLimits)
.Then(Charge);
```
Expand Down
20 changes: 10 additions & 10 deletions doc/UsagePatterns.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,29 @@ Un value object ne doit jamais entrer dans un état invalide. Que l’échec app

```csharp
// Contrat : renvoie une Temperature valide ou échoue — l'échec n'a pas sa place dans le type de retour de cette méthode.
public static Temperature FromKelvin(decimal kelvin) {
return TryFromKelvin(kelvin).GetResultOrThrow();
public static Temperature FromKelvinOrThrow(decimal kelvin) {
return FromKelvin(kelvin).GetResultOrThrow();
}
```

```csharp
// Contrat : une valeur hors limite fait partie du contrat normal — l'appelant doit la traiter explicitement.
public static Outcome<Temperature> TryFromKelvin(decimal kelvin) {
public static Outcome<Temperature> FromKelvin(decimal kelvin) {
if (kelvin < 0) { return Outcome<Temperature>.Failure(InvalidTemperatureError.BelowAbsoluteZero(kelvin, TemperatureUnit.Kelvin)); }

return Outcome<Temperature>.Success(new Temperature(kelvin));
}
```

Les deux rapportent exactement la même erreur documentée, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvin` ne répète pas la vérification : elle escalade l’échec de `TryFromKelvin` avec `GetResultOrThrow()`. Lorsque les deux contrats sont réellement utiles, centralisez la validation dans la version retournant un `Outcome`, puis dérivez-en la version levante — pas l’inverse.
Les deux rapportent exactement la même erreur documentée, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvinOrThrow` ne répète pas la vérification : elle escalade l’échec de `FromKelvin` avec `GetResultOrThrow()`. Lorsque les deux contrats sont réellement utiles, centralisez la validation dans la version retournant un `Outcome`, puis dérivez-en la version levante — pas l’inverse.

## 🧮 Opérations métier

Des objets métier valides peuvent néanmoins participer à une opération qui viole une règle. Le choix ici est une question de contrat d’API, pas de qui serait théoriquement en mesure de réagir — un appelant d’`Add` pourrait tout à fait capturer et traiter un écart levé, tout comme un appelant de `TryAdd` pourrait tout à fait ignorer un échec retourné. Ce qui diffère, c’est ce que chaque méthode promet à son appelant :
Des objets métier valides peuvent néanmoins participer à une opération qui viole une règle. Le choix ici est une question de contrat d’API, pas de qui serait théoriquement en mesure de réagir — un appelant d’`AddOrThrow` pourrait tout à fait capturer et traiter un écart levé, tout comme un appelant de `Add` pourrait tout à fait ignorer un échec retourné. Ce qui diffère, c’est ce que chaque méthode promet à son appelant :

```csharp
// Contrat : les montants fournis doivent déjà être exprimés dans la même devise.
public Amount Add(Amount other) {
public Amount AddOrThrow(Amount other) {
if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); }

return new Amount(Value + other.Value, Currency);
Expand All @@ -57,7 +57,7 @@ public Amount Add(Amount other) {

```csharp
// Contrat : une incompatibilité de devises est une issue attendue que l'appelant doit traiter.
public Outcome<Amount> TryAdd(Amount other) {
public Outcome<Amount> Add(Amount other) {
if (Currency != other.Currency) { return Outcome<Amount>.Failure(InvalidAmountOperationError.CurrencyMismatch(this, other)); }

return Outcome<Amount>.Success(new Amount(Value + other.Value, Currency));
Expand All @@ -73,7 +73,7 @@ Chaque élément d’un lot peut échouer indépendamment — mais le fait que c
```csharp
// Contrat : un échec par ligne est attendu et traité localement — le loguer et continuer.
foreach (string line in file) {
TryParseAmount(line).Finally(
ParseAmount(line).Finally(
onSuccess: Process,
onFailure: Log);
}
Expand All @@ -82,7 +82,7 @@ foreach (string line in file) {
```csharp
// Contrat : une ligne invalide invalide tout le fichier — arrêter immédiatement.
foreach (string line in file) {
Amount amount = TryParseAmount(line).GetResultOrThrow();
Amount amount = ParseAmount(line).GetResultOrThrow();
Process(amount);
}
```
Expand Down Expand Up @@ -148,7 +148,7 @@ Lorsque plusieurs opérations susceptibles d’échouer doivent s’enchaîner,

```csharp
Outcome<Receipt> result =
TryCreateAmount(value, currencyCode)
CreateAmount(value, currencyCode)
.Then(CheckLimits)
.Then(Charge);
```
Expand Down
2 changes: 1 addition & 1 deletion doc/WritingErrorMessages.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Good:

Avoid:

> “Currency validation failed in `Amount.Add` for order 42.”
> “Currency validation failed in `Amount.AddOrThrow` for order 42.”

The second example exposes implementation detail and an occurrence identifier that does not belong in a reusable public summary.

Expand Down
2 changes: 1 addition & 1 deletion doc/WritingErrorMessages.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Bon :

À éviter :

> « La validation des devises a échoué dans `Amount.Add` pour la commande 42. »
> « La validation des devises a échoué dans `Amount.AddOrThrow` pour la commande 42. »

Le second exemple expose un détail d’implémentation et un identifiant d’occurrence qui n’ont pas leur place dans un résumé public réutilisable.

Expand Down
Loading