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.
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 = FalseOption<T> sostituisce null. Un valore è Some(x) oppure None — il compilatore ti impedisce di ignorare l'assenza.
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 = AnonimoRailway-Oriented Programming: le operazioni si compongono con Bind. Se un passo fallisce, gli altri vengono saltati automaticamente — niente if-chain, niente try/catch.
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à registrataError ha tipi semantici che si mappano direttamente sugli HTTP status code. Non servono eccezioni custom — il tipo dice già tutto.
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 persaBindAsync e MapAsync funzionano esattamente come le versioni sync, ma con Task<Result<T>>. La pipeline resta lineare anche con operazioni asincrone.
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 trovatoCon MonadicSharp.DI ogni handler restituisce Result<T> — niente eccezioni, niente try/catch nel controller. Il mediator dispatcha query e command.
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 Pipelines —
BindAsync,MapAsync,TapAsyncin dettaglio - Ecosystem — pacchetti aggiuntivi per DI, Azure, AI, Recovery