Skip to content

Try it

Seleziona un esempio, leggi il codice, poi premi ▶ Run per vedere l'output.

Nessun backend — tutto gira in questa pagina.

Result<T> è il tipo fondamentale di MonadicSharp. Ogni operazione restituisce successo o fallimento — mai entrambi, mai null, mai eccezioni implicite.

csharp
using MonadicSharp;

// Crea un successo
Result<int> ok = Result.Success(42);

// Crea un fallimento
Result<int> fail = Result.Failure<int>(
    Error.Validation("Il valore deve essere positivo"));

// Estrai il valore con Match — sei obbligato a gestire entrambi i casi
string output1 = ok.Match(
    onSuccess: v     => $"Valore: {v}",
    onFailure: error => $"Errore: {error.Message}");

string output2 = fail.Match(
    onSuccess: v     => $"Valore: {v}",
    onFailure: error => $"Errore: {error.Message}");

Console.WriteLine(output1);
Console.WriteLine(output2);
Console.WriteLine($"ok.IsSuccess   = {ok.IsSuccess}");
Console.WriteLine($"fail.IsSuccess = {fail.IsSuccess}");
Valore: 42
Errore: Il valore deve essere positivo
ok.IsSuccess   = True
fail.IsSuccess = False

Option<T> sostituisce null. Un valore è Some(x) oppure None — il compilatore ti impedisce di ignorare l'assenza.

csharp
using MonadicSharp;

// Cerca un utente per ID — restituisce Option, non null
Option<string> FindUser(int id) =>
    id == 1 ? Option.Some("Alice") : Option.None<string>();

Option<string> found    = FindUser(1);
Option<string> notFound = FindUser(99);

// Map — trasforma il valore se presente, altrimenti None
Option<string> greeting = found.Map(name => $"Ciao, {name}!");

// GetValueOrDefault — valore di fallback esplicito
string name1 = found.GetValueOrDefault("Anonimo");
string name2 = notFound.GetValueOrDefault("Anonimo");

Console.WriteLine(greeting.GetValueOrDefault("Nessuno trovato"));
Console.WriteLine($"found    = {found}");
Console.WriteLine($"notFound = {notFound}");
Console.WriteLine($"name1    = {name1}");
Console.WriteLine($"name2    = {name2}");
Ciao, Alice!
found    = Some(Alice)
notFound = None
name1    = Alice
name2    = Anonimo

Railway-Oriented Programming: le operazioni si compongono con Bind. Se un passo fallisce, gli altri vengono saltati automaticamente — niente if-chain, niente try/catch.

csharp
using MonadicSharp;

Result<string> ValidateName(string name) =>
    string.IsNullOrWhiteSpace(name)
        ? Result.Failure<string>(Error.Validation("Nome obbligatorio"))
        : Result.Success(name.Trim());

Result<string> ValidateEmail(string email) =>
    email.Contains('@')
        ? Result.Success(email.ToLower())
        : Result.Failure<string>(Error.Validation("Email non valida"));

Result<string> CheckNotTaken(string email) =>
    email == "taken@example.com"
        ? Result.Failure<string>(Error.Conflict("Email già registrata"))
        : Result.Success(email);

// ─── Percorso felice ───
var happy = ValidateName("Alice")
    .Bind(_ => ValidateEmail("alice@example.com"))
    .Bind(CheckNotTaken)
    .Map(email => $"Utente creato con {email}");

// ─── Fallisce al primo step ───
var failName = ValidateName("")
    .Bind(_ => ValidateEmail("alice@example.com"))
    .Bind(CheckNotTaken);

// ─── Fallisce all'ultimo step ───
var failConflict = ValidateName("Bob")
    .Bind(_ => ValidateEmail("taken@example.com"))
    .Bind(CheckNotTaken);

Console.WriteLine(happy.Match(v => v, e => $"[{e.Type}] {e.Message}"));
Console.WriteLine(failName.Match(v => v, e => $"[{e.Type}] {e.Message}"));
Console.WriteLine(failConflict.Match(v => v, e => $"[{e.Type}] {e.Message}"));
Utente creato con alice@example.com
[Validation] Nome obbligatorio
[Conflict] Email già registrata

Error ha tipi semantici che si mappano direttamente sugli HTTP status code. Non servono eccezioni custom — il tipo dice già tutto.

csharp
using MonadicSharp;

// Tipi di errore predefiniti
var notFound   = Error.NotFound("Ordine 42 non trovato");
var validation = Error.Validation("Quantità deve essere > 0");
var conflict   = Error.Conflict("Email già registrata");
var forbidden  = Error.Forbidden("Accesso negato");
var unexpected = Error.Unexpected("Connessione al DB persa");

// Mappatura HTTP in un controller ASP.NET Core
IActionResult ToHttp(Error e) => e.Type switch
{
    ErrorType.NotFound   => NotFound(e.Message),      // 404
    ErrorType.Validation => BadRequest(e.Message),    // 400
    ErrorType.Conflict   => Conflict(e.Message),      // 409
    ErrorType.Forbidden  => Forbid(),                 // 403
    _                    => Problem(e.Message)         // 500
};

// Stampa tipo + messaggio
void Print(Error e) =>
    Console.WriteLine($"{e.Type,-12} → HTTP {HttpCode(e.Type),3}  |  {e.Message}");

int HttpCode(ErrorType t) => t switch {
    ErrorType.NotFound   => 404,
    ErrorType.Validation => 400,
    ErrorType.Conflict   => 409,
    ErrorType.Forbidden  => 403,
    _                    => 500
};

Print(notFound);
Print(validation);
Print(conflict);
Print(forbidden);
Print(unexpected);
NotFound     → HTTP 404  |  Ordine 42 non trovato
Validation   → HTTP 400  |  Quantità deve essere > 0
Conflict     → HTTP 409  |  Email già registrata
Forbidden    → HTTP 403  |  Accesso negato
Unexpected   → HTTP 500  |  Connessione al DB persa

BindAsync e MapAsync funzionano esattamente come le versioni sync, ma con Task<Result<T>>. La pipeline resta lineare anche con operazioni asincrone.

csharp
using MonadicSharp;

// Repository simulato — ogni metodo restituisce Task<Result<T>>
Task<Result<Guid>> FindUserAsync(string email) =>
    email == "alice@example.com"
        ? Task.FromResult(Result.Success(Guid.Parse("aaaaaaaa-0000-0000-0000-000000000001")))
        : Task.FromResult(Result.Failure<Guid>(Error.NotFound($"Utente {email} non trovato")));

Task<Result<decimal>> GetBalanceAsync(Guid userId) =>
    Task.FromResult(Result.Success(1_250.00m));

Task<Result<bool>> DebitAsync(Guid userId, decimal amount) =>
    amount <= 1_250.00m
        ? Task.FromResult(Result.Success(true))
        : Task.FromResult(Result.Failure<bool>(Error.Validation("Fondi insufficienti")));

// Pipeline: Find → GetBalance → Debit
// Se un passo fallisce, i successivi non vengono chiamati
var result = await FindUserAsync("alice@example.com")
    .BindAsync(userId  => GetBalanceAsync(userId))
    .BindAsync(balance => DebitAsync(Guid.Empty, balance - 500))
    .MapAsync(success  => "Pagamento completato");

Console.WriteLine(result.Match(msg => msg, e => $"[{e.Type}] {e.Message}"));

// Percorso di errore
var failed = await FindUserAsync("nobody@example.com")
    .BindAsync(userId  => GetBalanceAsync(userId))
    .BindAsync(balance => DebitAsync(Guid.Empty, balance));

Console.WriteLine(failed.Match(msg => msg, e => $"[{e.Type}] {e.Message}"));
Pagamento completato
[NotFound] Utente nobody@example.com non trovato

Con MonadicSharp.DI ogni handler restituisce Result<T> — niente eccezioni, niente try/catch nel controller. Il mediator dispatcha query e command.

csharp
using MonadicSharp;
using MonadicSharp.DI;

// ─── Query ───
public record GetOrderQuery(Guid OrderId) : IQuery<OrderDto>;

public class GetOrderHandler(IOrderRepository orders)
    : IQueryHandler<GetOrderQuery, OrderDto>
{
    public async Task<Result<OrderDto>> HandleAsync(
        GetOrderQuery query, CancellationToken ct)
    {
        var order = await orders.FindAsync(query.OrderId, ct);

        // Map restituisce None→Failure automaticamente con il messaggio
        return order.Match(
            some: o    => Result.Success(new OrderDto(o.Id, o.Status, o.Total)),
            none: ()   => Result.Failure<OrderDto>(
                              Error.NotFound($"Ordine {query.OrderId} non trovato")));
    }
}

// ─── Controller ───
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(Guid id, IMediator mediator)
{
    var result = await mediator.QueryAsync(new GetOrderQuery(id));

    return result.Match(
        onSuccess: dto   => Ok(dto),
        onFailure: error => error.ToActionResult());
        //                  ↑ estensione che mappa ErrorType → HTTP status
}
// GET /orders/aaaaaaaa-0000-0000-0000-000000000001
HTTP 200 OK
{
  "id":     "aaaaaaaa-0000-0000-0000-000000000001",
  "status": "Shipped",
  "total":  149.90
}

// GET /orders/ffffffff-ffff-ffff-ffff-ffffffffffff
HTTP 404 Not Found
{
  "error": "Ordine ffffffff-ffff-ffff-ffff-ffffffffffff non trovato"
}

Prossimi passi

  • Getting Started — aggiungi MonadicSharp al tuo progetto in 5 minuti
  • Core Types — documentazione completa di Result<T>, Option<T>, Error
  • Async PipelinesBindAsync, MapAsync, TapAsync in dettaglio
  • Ecosystem — pacchetti aggiuntivi per DI, Azure, AI, Recovery

Released under the MIT License.