Rotomeca.Rop

NuGet version CI License: ISC .NET Docs

Gestion d'erreurs typée à la Railway Oriented Programming pour C#. Inspiré du pattern Result<T, E> de Rust et des bonnes pratiques fonctionnelles, Rotomeca.Rop fournit une API cohérente, sûre et composable pour gérer les opérations pouvant échouer — sans exceptions intempestives, sans null silencieux.

Pensé pour s'aligner avec ses équivalents @rotomeca/rop (TypeScript) et rotomeca/rop (PHP), afin d'offrir des signatures et comportements quasi-identiques entre les trois langages.


Installation

dotnet add package Rotomeca.Rop

Compatibilité

Environnement Support
.NET Standard 2.1
.NET 8.0, 9.0
Source Link ✅ (navigation vers le code depuis le débogueur)
Symbols ✅ (.snupkg)
Nullable ✅ (activé)
Générateur ✅ ([Risky] / [Risky(Async = true)] + Roslyn)

Concepts clés

La librairie s'articule autour de quatre piliers :

Élément Rôle
IResult<TSuccess, TError> Résultat synchrone d'une opération — succès ou erreur, jamais les deux
IResultAsync<TSuccess, TError> Pendant asynchrone — pipeline sans await intermédiaire, directement awaitable
IError Erreur structurée : message, type sémantique, code applicatif, exception d'origine
[Risky] / [Risky(Async = true)] Génère automatiquement un wrapper Result.Try ou ResultAsync.TryAsync

Démarrage rapide

Créer un résultat synchrone

using Rotomeca.Rop;

// Succès
IResult<User> result = Result.Ok(new User(1, "Alice"));

// Erreur structurée
IResult<User> result = Result.Fail<User>(
    new Error("Utilisateur introuvable", ErrorType.NotFound, code: "USR_404"));

// Capture d'exception
IResult<int, IError> result = Result.Try(() => int.Parse(input));

Créer un résultat asynchrone

// Depuis une Task<T>
IResultAsync<User> result = ResultAsync.Ok(FetchUserFromDbAsync(id));

// Capture d'exception synchrone (task froide)
IResultAsync<User, IError> result = ResultAsync.Try(() => new Task<User>(...));

// Capture d'exception async complète (méthodes async)
Task<IResultAsync<User, IError>> result = ResultAsync.TryAsync(() => FetchUserAsync(id));

Composer un pipeline synchrone

var message = FindUser(userId)
    .AndThen(user => PlaceOrder(user, amount))
    .Map(order => order.Amount)
    .Match(
        Ok:  amount => $"Commande confirmée : {amount:C}",
        Err: error  => $"Échec ({error.Code}) : {error.Message}"
    );

Composer un pipeline asynchrone

// Chaînage sans await intermédiaire — un seul await à la fin
IResult<decimal> result = await FindUserAsync(userId)
    .AndThen(user => PlaceOrderAsync(user, amount))
    .MapAsync(order => EnrichOrderAsync(order))
    .Tap(order => logger.Info("Commande {id}", order.Id));

string message = result.Match(
    Ok:  amount => $"Confirmée : {amount:C}",
    Err: error  => $"Échec : {error.Message}"
);

Générer un wrapper [Risky]

public partial class UserService
{
    // Génère : public IResult<User, IError> Fetch(int id)
    [Risky]
    private User _Fetch(int id)
        => _repository.GetById(id) ?? throw new KeyNotFoundException();

    // Génère : public Task<IResultAsync<User, IError>> FetchAsync(int id)
    [Risky(Async = true)]
    private async Task<User> _FetchAsync(int id)
    {
        var user = await _repository.GetByIdAsync(id);
        return user ?? throw new KeyNotFoundException();
    }
}

Structure du dépôt

Rotomeca.Rop/
├── src/
│   ├── Rotomeca.Rop.Core/        # Librairie principale (IResult, IResultAsync, Error…)
│   └── Rotomeca.Rop.Generators/  # Générateur Roslyn ([Risky] sync et async)
└── tests/
    └── Rotomeca.Rop.Core.Tests/  # Suite de tests xUnit (sync + async)

ErrorType — catégories sémantiques

ErrorType est un record struct extensible. Les catégories prédéfinies couvrent les cas courants :

Valeur Usage
Unexpected Erreur inattendue ou non catégorisée (défaut)
Validation Données d'entrée invalides
NotFound Ressource introuvable
Unauthorized Accès non autorisé ou permissions manquantes
Conflict Conflit d'état sur la ressource
public static class AppErrors
{
    public static readonly ErrorType PaymentRequired = new("PaymentRequired");
    public static readonly ErrorType RateLimit       = new("RateLimit");
}

Référence des exports publics

// Factory synchrone
using Rotomeca.Rop;
Result.Ok<T>(value)           Result.Ok<T, E>(value)
Result.Fail<T>(error)         Result.Fail<T, E>(error)
Result.Fail(error)            Result.Try(fn)
Result.Throw(...)

// Factory asynchrone
ResultAsync.Ok<T>(task)       ResultAsync.Ok<T, E>(task)
ResultAsync.Fail<T>(task)     ResultAsync.Fail<T, E>(task)
ResultAsync.Try(fn)           ResultAsync.TryAsync(fn)
ResultAsync.Throw(...)

// Interfaces synchrones
using Rotomeca.Rop.Interfaces;
IResult<TSuccess, TError>     IResult<TSuccess>     IEmptyResult

// Interfaces asynchrones
IResultAsync<TSuccess, TError>     IResultAsync<TSuccess>     IEmptyResultAsync

// Modèle d'erreur
IError     Error     Error<TData>     ErrorType

// Extensions
using Rotomeca.Rop.Extensions;
.AsResult()    .WithError(mapper)    .As(value)    .As(factory)

// Générateur
[Risky]                  // wrapper Result.Try (sync)
[Risky(Async = true)]    // wrapper ResultAsync.TryAsync (async)

Ecosystème Rotomeca

Package Langage Description
Rotomeca.Rop C# Ce package
@rotomeca/rop TypeScript Gestion d'erreurs typée (Result<T, E>)
@rotomeca/event TypeScript Système d'événements typés à la C#
@rotomeca/utils TypeScript Fonctions pures, types brandés et helpers
@rotomeca/jsenumerable TypeScript LINQ lazy en TypeScript

Contribuer

git clone https://github.com/Rotomeca/rop.git
cd rop
dotnet restore
dotnet test

Les contributions sont les bienvenues via Pull Request sur la branche dev.


Note sur l'utilisation de l'IA

L'intégralité du code de ce projet a d'abord été écrite à la main en essayant d'avoir le C# le plus propre possible. L'IA a ensuite été utilisée pour :

  • Proposer des axes d'amélioration et de refactorisation si besoin, après relecture de ses modifications par mes soins
  • La documentation et les README — j'ai toujours été une bille en documentation, je trouve celle de l'IA lisible et explicite ; elle a toujours été relue et validée par mes soins
  • Les tests unitaires — tester, c'est facile, mais présenter des tests unitaires, c'est complexe (de mon point de vue) ; l'IA a dans un premier temps généré les tests, je les ai parcourus pour les comprendre et les corriger au besoin
  • La CI/CD — vu que ce n'est pas mon domaine, mais ça permet d'apprendre beaucoup 👍

Sa principale contribution a donc été de m'accompagner sur les points qui me sont lacunaires.


Licence

ISC © Rotomeca