An open API service indexing awesome lists of open source software.

https://github.com/safwa1/sharpresults

A lightweight, zero-dependency C# library that implements the Result and Option types for more explicit and type-safe error handling. SharpResults helps you avoid exceptions for control flow and makes success/failure and presence/absence states explicit in your code.
https://github.com/safwa1/sharpresults

csharp dotnet error-handling extension-methods fluent-api functional-programming library monads nuget optianal result-type sharpresults

Last synced: 8 months ago
JSON representation

A lightweight, zero-dependency C# library that implements the Result and Option types for more explicit and type-safe error handling. SharpResults helps you avoid exceptions for control flow and makes success/failure and presence/absence states explicit in your code.

Awesome Lists containing this project

README

          

# SharpResults

A lightweight, zero-dependency C# library that implements the Result and Option types for more explicit and type-safe error handling. SharpResults helps you avoid exceptions for control flow and makes success/failure and presence/absence states explicit in your code.

![SharpResults Logo](https://img.shields.io/badge/SharpResults-Type--Safe%20Error%20Handling-blue)
[![NuGet](https://img.shields.io/nuget/v/SharpResults.svg)](https://www.nuget.org/packages/SharpResults/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Downloads](https://img.shields.io/nuget/dt/SharpResults.svg)](https://www.nuget.org/packages/SharpResults/)

## 🚀 Features

- ðŸ›Ąïļ **Type-safe error handling** - No more unexpected exceptions
- 🔄 **Chainable operations** - Fluent API for transforming and combining results
- ðŸ§Đ **LINQ integration** - Works with C# query syntax
- ⚡ **Async support** - First-class support for async/await operations
- 🔍 **Comprehensive API** - Rich set of methods for working with results
- ðŸŠķ **Lightweight** - Zero dependencies
- ðŸŽŊ **Multi-targeting** - Supports .NET 8.0 and .NET 9.0
- 📊 **Performance optimized** - Minimal overhead compared to traditional exception handling

## 📋 Table of Contents

- [Installation](#-installation)
- [Basic Usage](#basic-usage)
- [Advanced Usage](#advanced-usage)
- [Asynchronous Operations](#asynchronous-operations)
- [Real-World Examples](#real-world-examples)
- [Performance Considerations](#performance-considerations)
- [Comparison with Other Libraries](#comparison-with-other-libraries)
- [Contributing](#contributing)
- [License](#license)

## ðŸ“Ķ Installation

### Package Manager

```
Install-Package SharpResults
```

### .NET CLI

```
dotnet add package SharpResults
```

## Basic Usage

### Creating Results

```csharp
using SharpResults;
using SharpResults.Types;

// Create a successful result
Result success = Result.Ok(42);

// Create a failed result with an exception
Result failure = Result.Err(new Exception("Something went wrong"));

// Create a failed result with just a message
Result failureWithMessage = Result.Err("Invalid input");

// Implicit conversion from value to successful result
Result implicitSuccess = 42;
```

### Checking Result State

```csharp
Result result = GetSomeResult();

// Using properties
if (result.IsOk)
{
// Use result.Value safely here
Console.WriteLine($"Got value: {result.Value}");
}
else
{
// Handle the error
Console.WriteLine($"Error: {result.Exception.Message}");
}

// Using pattern matching (C# 8.0+)
switch (result)
{
case { IsOk: true } ok:
Console.WriteLine($"Success: {ok.Value}");
break;
case { IsErr: true } err:
Console.WriteLine($"Error: {err.Exception.Message}");
break;
}

// Using deconstruction syntax
var (isOk, value, exception) = result;
if (isOk)
{
Console.WriteLine($"Value: {value}");
}
```

### Transforming Results

```csharp
// Map transforms the value inside a successful result
Result result = Result.Ok(42);
Result mapped = result.Map(x => x.ToString());

// AndThen (or FlatMap) chains result-returning operations
Result parsed = Result.Ok("42")
.AndThen(str => {
if (int.TryParse(str, out int value))
return Result.Ok(value);
return Result.Err("Parsing failed");
});

// LINQ syntax support
Result computation =
from x in Result.Ok(10)
from y in Result.Ok(5)
select x + y; // Result.Ok(15)
```

### Handling Errors

```csharp
// Recover from errors
Result recovered = Result.Err("Original error")
.OrElse(ex => Result.Ok(42));

// Provide default values
int value = Result.Err("Error")
.UnwrapOr(42); // 42

// Use a fallback function
int computed = Result.Err(new Exception("Failed"))
.UnwrapOrElse(ex => 42); // 42

// Map errors to different error types
Result mappedError = Result.Err("Database error")
.MapErr(msg => new CustomError(ErrorType.Database, msg));
```

### Pattern Matching with Match

```csharp
// Transform both success and error cases
string message = result.Match(
ok: value => $"Success: {value}",
err: ex => $"Error: {ex.Message}"
);

// Execute actions based on result state
result.Match(
ok: value => SaveToDatabase(value),
err: ex => LogError(ex)
);
```

## Advanced Usage

### Custom Error Types

Use `Result` when you want to use custom error types instead of exceptions:

```csharp
// Define a custom error type
public enum ApiError
{
NotFound,
Unauthorized,
ServerError
}

// Create results with custom error types
Result GetUser(int id)
{
if (id <= 0)
return ApiError.NotFound; // Implicit conversion

if (!IsAuthenticated())
return Result.Err(ApiError.Unauthorized);

try
{
var user = _repository.GetUser(id);
return user != null
? Result.Ok(user)
: ApiError.NotFound;
}
catch
{
return ApiError.ServerError;
}
}

// Usage
var result = GetUser(42);

// Pattern matching with custom errors
string message = result.Match(
ok: user => $"Found user: {user.Name}",
err: error => error switch
{
ApiError.NotFound => "User not found",
ApiError.Unauthorized => "Please login first",
ApiError.ServerError => "Server error occurred",
_ => "Unknown error"
}
);
```

### Working with Void Returns

Use `Unit` as a return type for operations that don't return a value:

```csharp
// For methods that don't return a value
Result SaveData(string data)
{
try
{
// Save data to database
File.WriteAllText("data.txt", data);
return Result.Ok(Unit.Value);
}
catch (Exception ex)
{
return Result.Err(ex);
}
}

// Usage
Result saveResult = SaveData("important data");
if (saveResult.IsOk)
{
Console.WriteLine("Data saved successfully");
}
```

### Try-Catch Alternative

Use `Result.From` to automatically catch exceptions:

```csharp
// Automatically catches exceptions
Result divideResult = Result.From(() => 10 / 0);
// divideResult will be an Err with DivideByZeroException

// For void-returning methods
Result writeResult = Result.From(() => File.WriteAllText("file.txt", "content"));

// With custom error mapping
Result customErrorResult = Result.From(
() => ParseComplexData(),
ex => $"Parsing error: {ex.Message}"
);
```

### Chaining Multiple Operations

```csharp
Result GetProcessedData(string input)
{
return Result.Ok(input)
.AndThen(ValidateInput)
.Map(data => data.ToUpper())
.AndThen(ProcessData)
.Inspect(data => Console.WriteLine($"Processing complete: {data}"))
.InspectErr(ex => Console.WriteLine($"Error occurred: {ex.Message}"));
}

Result ValidateInput(string input)
{
return string.IsNullOrEmpty(input)
? Result.Err("Input cannot be empty")
: Result.Ok(input);
}

Result ProcessData(string data)
{
// Process the data
return Result.Ok($"Processed: {data}");
}
```

### Combining Multiple Results

```csharp
// Combine multiple results into one
Result<(int, string, bool)> combined = Result.Combine(
Result.Ok(42),
Result.Ok("hello"),
Result.Ok(true)
);

// If any result is an error, the combined result will be an error
Result<(int, string)> partialError = Result.Combine(
Result.Ok(42),
Result.Err("Something went wrong")
); // Will be Err with "Something went wrong"

// Combine a collection of results
IEnumerable> results = new[] { Result.Ok(1), Result.Ok(2), Result.Ok(3) };
Result> collectionResult = Result.Combine(results);
// Result.Ok([1, 2, 3])
```

## Asynchronous Operations

SharpResults provides first-class support for asynchronous operations:

```csharp
// Async methods returning Result
public async Task> GetUserAsync(int id)
{
try
{
var user = await _userRepository.GetByIdAsync(id);
return user != null
? Result.Ok(user)
: Result.Err("User not found");
}
catch (Exception ex)
{
return Result.Err(ex);
}
}

// Chaining async operations
public async Task> PlaceOrderAsync(int userId, int productId)
{
return await GetUserAsync(userId)
.MapAsync(async user => {
// Validate user can place orders
if (!user.CanPlaceOrders)
return Result.Err("User cannot place orders");
return Result.Ok(user);
})
.AndThenAsync(async user => await GetProductAsync(productId))
.AndThenAsync(async product => {
if (!product.InStock)
return Result.Err("Product out of stock");

var confirmation = await _orderService.CreateOrderAsync(userId, productId);
return Result.Ok(confirmation);
});
}

// Using LINQ with async results
public async Task> GetOrderSummaryAsync(int orderId)
{
var orderResult = await GetOrderAsync(orderId);

return await (from order in orderResult
from customer in await GetCustomerAsync(order.CustomerId)
from items in await GetOrderItemsAsync(orderId)
select new OrderSummary
{
OrderId = order.Id,
CustomerName = customer.Name,
Items = items,
Total = items.Sum(i => i.Price)
});
}
```

## Real-World Examples

### Web API Controller

```csharp
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;

public UsersController(IUserService userService)
{
_userService = userService;
}

[HttpGet("{id}")]
public async Task GetUser(int id)
{
var result = await _userService.GetUserAsync(id);

return result.Match(
ok: user => Ok(user),
err: ex => ex.Message == "User not found"
? NotFound(ex.Message)
: StatusCode(500, "An error occurred while processing your request")
);
}

[HttpPost]
public async Task CreateUser(CreateUserRequest request)
{
var result = await _userService.CreateUserAsync(request);

return result.Match(
ok: user => CreatedAtAction(nameof(GetUser), new { id = user.Id }, user),
err: ex => BadRequest(ex.Message)
);
}
}
```

### Domain Logic with Validation

```csharp
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly ICustomerRepository _customerRepository;

// Constructor with DI

public async Task> PlaceOrderAsync(OrderRequest request)
{
// Validate request
var validationResult = ValidateOrderRequest(request);
if (validationResult.IsErr)
return Result.Err(validationResult.Exception);

// Check if customer exists
var customerResult = await _customerRepository.GetByIdAsync(request.CustomerId);
if (customerResult.IsErr)
return Result.Err($"Customer not found: {request.CustomerId}");

// Check if all products exist and are in stock
var productsResult = await CheckProductsAvailabilityAsync(request.Items);
if (productsResult.IsErr)
return productsResult.MapErr();

// Create order
var order = new Order
{
CustomerId = request.CustomerId,
Items = request.Items.Select(i => new OrderItem { ProductId = i.ProductId, Quantity = i.Quantity }).ToList(),
OrderDate = DateTime.UtcNow,
Status = OrderStatus.Pending
};

// Save order
try
{
await _orderRepository.CreateAsync(order);
return Result.Ok(order);
}
catch (Exception ex)
{
return Result.Err(ex);
}
}

private Result ValidateOrderRequest(OrderRequest request)
{
if (request == null)
return Result.Err("Order request cannot be null");

if (request.CustomerId <= 0)
return Result.Err("Invalid customer ID");

if (request.Items == null || !request.Items.Any())
return Result.Err("Order must contain at least one item");

if (request.Items.Any(i => i.Quantity <= 0))
return Result.Err("All items must have a quantity greater than zero");

return Result.Ok(Unit.Value);
}

private async Task> CheckProductsAvailabilityAsync(IEnumerable items)
{
foreach (var item in items)
{
var productResult = await _productRepository.GetByIdAsync(item.ProductId);
if (productResult.IsErr)
return Result.Err($"Product not found: {item.ProductId}");

var product = productResult.Value;
if (product.StockQuantity < item.Quantity)
return Result.Err($"Insufficient stock for product {product.Name}. Available: {product.StockQuantity}, Requested: {item.Quantity}");
}

return Result.Ok(Unit.Value);
}
}
```

## Performance Considerations

SharpResults is designed to be lightweight and efficient. Here are some performance considerations:

- **Avoid exceptions for control flow**: Using SharpResults instead of throwing exceptions can significantly improve performance in error-prone code paths.
- **Lazy error handling**: Error messages and exceptions are only created when needed.
- **Minimal allocations**: The library minimizes heap allocations where possible.
- **Struct-based implementation**: For performance-critical code, consider using the struct-based variants of Result types.

## Comparison with Other Libraries

### ⚖ïļ TL;DR — Summary

| Feature / Library | SharpResults | FluentResults | OneOf | CSharpFunctionalExtensions |
|------------------|----------------------|---------------|-------|-----------------------------|
| ✅ Strong Result/Error typing | ✔ïļ Yes | ✔ïļ Yes | ❌ No | ✔ïļ Yes |
| ✅ Rich extension methods | ✔ïļ Yes | ✔ïļ Partial | ❌ Minimal | ✔ïļ Yes |
| ✅ Exception capturing | ✔ïļ Yes | ✔ïļ Yes | ❌ No | ✔ïļ Yes |
| ✅ Pattern matching support | ✔ïļ With Match | ✔ïļ With ResultType | ✔ïļ Native | ❌ Manual |
| ✅ Null safety / value handling | ✔ïļ Yes | ✔ïļ Yes | ⚠ïļ Riskier | ✔ïļ Yes |
| ✅ Simplicity & minimalism | ✔ïļ Lean and clean | ❌ Heavy | ✔ïļ Minimal | ❌ Verbose |
| ✅ Control over error types | ✔ïļ Generic errors (TError) | ✔ïļ Message objects | ❌ Not applicable | ✔ïļ Generic |
| 🛠ïļ IDE-friendliness (C# tooling) | ✔ïļ Yes | ✔ïļ Yes | ⚠ïļ Limited | ✔ïļ Yes |

### Detailed Feature Comparison

| Feature | SharpResults | OneOf | CSharpFunctionalExtensions | FluentResults |
|---------|-------------|-------|----------------------------|---------------|
| Custom Error Types | ✅ | ✅ | ✅ | ✅ |
| LINQ Support | ✅ | ❌ | ✅ | ❌ |
| Async Support | ✅ | ❌ | ✅ | ✅ |
| Pattern Matching | ✅ | ✅ | ❌ | ❌ |
| Implicit Conversions | ✅ | ✅ | ✅ | ❌ |
| Deconstruction | ✅ | ✅ | ✅ | ❌ |
| Multiple Error Collection | ✅ | ❌ | ❌ | ✅ |
| Zero Dependencies | ✅ | ✅ | ✅ | ✅ |
| .NET Standard 2.0+ | ✅ | ✅ | ✅ | ✅ |

## Contributing

Contributions are welcome! Here's how you can contribute:

1. **Fork the repository**
2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
3. **Commit your changes**: `git commit -m 'Add some amazing feature'`
4. **Push to the branch**: `git push origin feature/amazing-feature`
5. **Open a Pull Request**

### Development Guidelines

- Follow the existing code style and conventions
- Add unit tests for new features
- Update documentation for any changes
- Ensure all tests pass before submitting a PR

## License

This project is licensed under the MIT License - see the LICENSE file for details.