C#
Utiliser le Result Pattern en C# au lieu de retourner null
Le Result Pattern rend explicites les états attendus de réussite et d’échec, offrant une alternative plus claire au retour de null ou au déclenchement d’exceptions pour les résultats métier courants.
J’ai découvert le Result Pattern en travaillant avec .NET, et il a changé ma façon d’envisager les méthodes susceptibles d’échouer. Au lieu de retourner null et de laisser le code appelant l’interpréter, la méthode retourne un objet qui décrit soit une réussite, soit un échec.
Cela ne signifie pas que toutes les valeurs nullables ou toutes les exceptions doivent disparaître. Ce pattern est particulièrement utile lorsque l’échec est un résultat attendu et que le code appelant doit comprendre ce qui s’est passé.
La limite du retour de null
Prenons une méthode de repository qui retourne null lorsqu’elle ne trouve pas un utilisateur. Le code appelant peut vérifier si la valeur est null, mais celle-ci n’explique pas la raison. L’identifiant était-il invalide ? L’utilisateur était-il introuvable ? Une autre validation a-t-elle échoué ?
Les nullable reference types améliorent cette situation en nous permettant d’indiquer si une référence est censée accepter null. Le compilateur C# utilise ensuite l’analyse statique pour avertir des affectations nulles et des déréférencements potentiels. Cependant, il s’agit d’une fonctionnalité de compilation ; elle ne crée pas de type runtime distinct et n’ajoute aucune validation runtime. (learn.microsoft.com)
Un Result<T> donne du sens au résultat de l’opération. Au lieu de retourner User?, une méthode peut retourner un Result<User> contenant soit l’utilisateur, soit une erreur précise.
Représenter les erreurs
La première partie est un petit type d’erreur :
public record Error(string Code, string Message)
{
public static Error None = new(string.Empty, string.Empty);
public static Error NullValue = new("Error.NullValue", "Um valor nulo foi fornecido.");
}
Le code fournit un identifiant stable pour les décisions programmatiques ainsi qu’un message qui peut être journalisé ou affiché. Error.None représente une opération réussie, pour laquelle aucune erreur n’existe.
Créer le type Result
Le Result non générique représente les opérations qui réussissent sans retourner de valeur :
public class Result
{
protected Result(bool isSuccess, Error error)
{
switch (isSuccess)
{
case true when error != Error.None:
throw new InvalidOperationException();
case false when error == Error.None:
throw new InvalidOperationException();
default:
IsSuccess = isSuccess;
Error = error;
break;
}
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
public static Result<T> Success<T>(T value) => new(value, true, Error.None);
public static Result<T> Failure<T>(Error error) => new(default, false, error);
public static Result<T> Create<T>(T? value) =>
value is not null ? Success(value) : Failure<T>(Error.NullValue);
}
Le constructeur protège deux règles importantes : un résultat réussi ne peut pas contenir d’erreur, et un résultat en échec doit en contenir une. Le maintien de la validité de ces états évite les objets contradictoires, comme un résultat réussi accompagné d’un message d’erreur.
Pour les opérations qui retournent des données, la version générique stocke la valeur obtenue en cas de réussite :
public class Result<T> : Result
{
private readonly T? _value;
protected internal Result(T? value, bool isSuccess, Error error) : base(isSuccess, error)
=> _value = value;
[NotNull]
public T Value => _value! ?? throw new InvalidOperationException("Result has no value");
public static implicit operator Result<T>(T? value) => Create(value);
}
La propriété Value est disponible pour les résultats réussis. Y accéder après un échec déclenche une InvalidOperationException, les consommateurs doivent donc d’abord examiner l’état du résultat. Ce pattern rend le contrat plus clair, mais il repose toujours sur une utilisation correcte au niveau du code appelant.
Retourner un utilisateur sans retourner null
Une méthode de recherche peut désormais communiquer explicitement différents échecs attendus :
public Result<User> GetUserById(int userId)
{
if (userId <= 0)
return Result.Failure<User>(Error.InvalidUserId);
var user = _userRepository.FindById(userId);
if (user == null)
return Result.Failure<User>(Error.UserNotFound);
return Result.Success(user);
}
En supposant que InvalidUserId et UserNotFound soient définis dans le catalogue d’erreurs, le type de retour indique aux consommateurs que la méthode possède deux états possibles. Le code appelant peut les gérer directement :
var result = GetUserById(123);
if (result.IsSuccess)
{
Console.WriteLine($"User found: {result.Value.Name}");
}
else
{
Console.WriteLine($"Failed to retrieve user: {result.Error.Message}");
}
C’est plus descriptif que de vérifier si un User retourné est null. Cela permet également de conserver les résultats métier attendus dans le flux de contrôle normal.
Result ne remplace pas les exceptions
Les exceptions restent appropriées pour les situations inattendues qui ne peuvent pas être traitées dans le cadre du fonctionnement normal. Les recommandations de .NET préconisent d’éviter les exceptions pour les situations courantes, tout en utilisant la gestion des exceptions pour les événements réellement exceptionnels. (learn.microsoft.com)
Une distinction pratique consiste à :
- Retourner un résultat pour les échecs de validation, les enregistrements manquants, les conflits et les autres résultats attendus.
- Déclencher des exceptions pour les erreurs de programmation, un état d’objet invalide ou les défaillances d’infrastructure inattendues que la couche actuelle ne peut pas gérer de manière pertinente.
Le Result Pattern ne vise pas principalement à éliminer null ou les exceptions partout. Il s’agit de donner un type explicite aux échecs attendus et de rendre les contrats des méthodes plus faciles à comprendre.
Deux autres présentations pratiques de ce pattern sont disponibles sur https://www.red-gate.com/simple-talk/development/dotnet-development/the-result-pattern-in-asp-net-core-minimal-apis/ et https://medium.com/@emrecantopaloglu/the-result-pattern-in-net-a-simple-guide-73a8f1b89d73.
Références
- Nullable reference types - C# reference | Microsoft Learn — learn.microsoft.com
- Best practices for exceptions - .NET | Microsoft Learn — learn.microsoft.com
- The Result Pattern In Asp Net Core Minimal Apis — red-gate.com
- The Result Pattern In Net A Simple Guide 73A8f1b89d73 — medium.com