Getting Started with Pragmatic.Result
This guide will get you up and running with Pragmatic.Result in 5 minutes.
Installation
Section titled “Installation”dotnet add package Pragmatic.Resultdotnet add package Pragmatic.Result.AspNetCore # For ASP.NET Core integrationdotnet add package Pragmatic.Result.Analyzers # PRAG0001: warns on unsafe .Value accessQuick Start
Section titled “Quick Start”1. Basic Result Usage
Section titled “1. Basic Result Usage”using Pragmatic.Result;using Pragmatic.Result.Http;
public class UserService{ public Result<User, NotFoundError> GetUser(int id) { var user = _db.Users.Find(id); if (user is null) return NotFoundError.Create("User", id);
return user; // Implicit conversion to Success }}2. Handle Results
Section titled “2. Handle Results”var result = userService.GetUser(42);
// Option 1: Pattern matching with Matchvar response = result.Match( user => $"Hello, {user.Name}!", error => $"Error: {error.Code}");
// Option 2: TryGet patternif (result.TryGetValue(out var user)){ Console.WriteLine($"Found: {user.Name}");}
// Option 3: Check IsSuccess/IsFailureif (result.IsSuccess){ var user = result.Value;}3. Pipeline Operations
Section titled “3. Pipeline Operations”When a pipeline mixes failure modes (not found, business rule), type the result over IError:
using Pragmatic.Result.Extensions; // Map, Ensure, Tap, OnFailure, ...
public Result<UserDto, IError> GetActiveUser(int id){ Result<User, IError> result = _db.Users.Find(id) is { } user ? user : NotFoundError.Create("User", id);
return result .Ensure(u => u.IsActive, u => BusinessRuleError.Inactive("User")) .Map(u => u.ToDto()) .Tap(dto => _logger.LogInformation("User retrieved: {Id}", dto.Id)) .OnFailure(e => _logger.LogWarning("Failed: {Code}", e.Code));}Each step runs only while the result is a success; the first failure short-circuits the rest.
4. ASP.NET Core Integration
Section titled “4. ASP.NET Core Integration”Register the services, then enable automatic Result-to-HTTP conversion.
Minimal APIs – add WithResultHandling() to a route group:
using Pragmatic.Result.AspNetCore;
builder.Services.AddPragmaticResult();
var app = builder.Build();
app.MapGroup("").WithResultHandling() .MapGet("/users/{id}", async (int id, UserService svc) => await svc.GetUserAsync(id));// Returns 200 OK with user, or 404 ProblemDetails automatically// VoidResult success -> 204 No ContentControllers – add ResultActionFilter as a global filter:
builder.Services.AddPragmaticResult();builder.Services.AddControllers(options => options.Filters.Add<ResultActionFilter>());
// Controller[HttpGet("{id}")]public async Task<Result<UserDto, NotFoundError>> GetUser(int id){ return await _userService.GetUserAsync(id);}// Returns 200 OK with user, or 404 ProblemDetails automaticallyOpt out a specific endpoint with [SkipResultHandling].
Core Concepts
Section titled “Core Concepts”Result Types
Section titled “Result Types”| Type | Use Case |
|---|---|
Result<TValue, TError> |
Operations that return a value or fail |
Result<TValue, TError1, ..., TError8> |
Multiple distinct failure modes (2-8 error types) |
VoidResult<TError> |
Operations that succeed or fail (no value) |
Maybe<T> |
Optional values (Some or None) |
Error Types
Section titled “Error Types”All in the Pragmatic.Result.Http namespace:
| Error | HTTP Status | Use Case |
|---|---|---|
BadRequestError |
400 | Malformed request, missing header |
UnauthorizedError |
401 | Authentication required |
ForbiddenError |
403 | Insufficient permissions |
NotFoundError |
404 | Entity not found |
ConflictError |
409 | Duplicate key, concurrency |
BusinessRuleError |
422 | Business rule prevents processing |
InternalServerError |
500 | Unexpected failure |
DependencyError |
502/503/504 | External service failure (transient) |
Key Methods
Section titled “Key Methods”// Transformationresult.Map(x => Transform(x)) // Transform success valueresult.MapError(e => NewError(e)) // Transform errorresult.Bind(x => GetOther(x)) // Chain operations
// Side effects (don't change result)result.Tap(x => Log(x)) // Execute on successresult.OnSuccess(x => Notify(x)) // Alias for Tapresult.OnFailure(e => LogError(e)) // Execute on failure
// Validationresult.Ensure(x => x.IsValid, x => BadRequestError.Create("Invalid"))
// Recoveryresult.OrElse(e => FallbackResult()) // Fallback Result on failureresult.Recover(e => DefaultValue()) // Fallback value on failure (always Success)result.GetValueOrDefault(fallback) // Get value or fallbackMulti-Error Types
Section titled “Multi-Error Types”For operations that can fail in different ways. Variants for 2 to 8 error types ship with the package; each error type gets its own Match handler:
public Result<User, NotFoundError, ForbiddenError> GetUser(int id, ClaimsPrincipal user){ var entity = _db.Users.Find(id); if (entity is null) return NotFoundError.Create("User", id);
if (!user.CanView(entity)) return new ForbiddenError();
return entity;}
// Handle with Matchresult.Match( user => Ok(user), notFound => NotFound(), forbidden => Forbid());Async Operations
Section titled “Async Operations”var result = await GetUserAsync(id) .MapAsync(u => EnrichUserAsync(u)) .BindAsync(u => ValidateAsync(u)) .TapAsync(u => SendNotificationAsync(u));Best Practices
Section titled “Best Practices”- Use specific error types -
NotFoundErrorover genericError - Prefer pipelines - Chain operations with Map/Bind/Ensure
- Side effects in Tap - Logging, metrics, notifications
- Validate early - Use Ensure to add validation steps
- Handle all cases - Use Match for exhaustive handling
Next Steps
Section titled “Next Steps”- Architecture and Core Concepts - Why Result exists and how to choose the right type
- API Reference - Complete method documentation
- Migration Guide - Migrate from exceptions/other libraries
- Localization - Localize error messages
- Troubleshooting - Common issues and solutions
