A lightweight .NET library that provides a Try/Result pattern for executing actions and functions with automatic exception handling. Instead of try/catch blocks scattered throughout your code, wrap operations in Try.Run() and get back a Result indicating success or failure.
dotnet add package Wolfgang.TryPatternInstall-Package Wolfgang.TryPatternThis project is licensed under the MIT License.
- GitHub Repository: https://github.com/Chris-Wolfgang/Try-Pattern
- API Documentation: https://chris-wolfgang.github.io/Try-Pattern/
- API Reference: https://chris-wolfgang.github.io/Try-Pattern/api/
- Contributing Guide: CONTRIBUTING.md
- Changelog: CHANGELOG.md
using Wolfgang.TryPattern;
var result = Try.Run(() => File.Delete("temp.txt"));
if (result.Succeeded)
{
Console.WriteLine("File deleted.");
}
else
{
Console.WriteLine($"Failed: {result.ErrorMessage}");
}var result = Try.Run(() => int.Parse("42"));
if (result.Succeeded)
{
Console.WriteLine($"Parsed value: {result.Value}");
}
else
{
Console.WriteLine($"Parse failed: {result.ErrorMessage}");
}var result = await Try.RunAsync(async () =>
{
var response = await httpClient.GetStringAsync("https://example.com");
return response;
});
if (result.Failed)
{
Console.WriteLine($"Request failed: {result.ErrorMessage}");
}Try.RunAsync(Action, CancellationToken) runs a synchronous action on the thread pool via Task.Run. The token cancels the task before it starts; once running, cancellation is cooperative (your code must check the token). OperationCanceledException is always rethrown, never captured as a Result.
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result = await Try.RunAsync(() =>
{
foreach (var item in largeDataSet)
{
cts.Token.ThrowIfCancellationRequested();
Process(item);
}
}, cts.Token);| Feature | Description |
|---|---|
Try.Run(Action) |
Execute an action, return Result |
Try.Run<T>(Func<T>) |
Execute a function, return Result<T> with the value |
Try.RunAsync(Action, CancellationToken) |
Run a synchronous action on the thread pool with cooperative cancellation |
Try.RunAsync<T>(Func<Task<T>>, CancellationToken) |
Async function execution with cancellation support |
Result.Success() |
Create a successful result |
Result.Failure(message) |
Create a failed result with an error message |
Result<T>.Success(value) |
Create a successful result with a value |
Result<T>.Failure(message) |
Create a failed result |
Result.Flatten(results) |
Combine multiple results into one |
Result.AnyFailed(results) |
Check if any results failed |
Result.AllSucceeded(results) |
Check if all results succeeded |
| Property | Description |
|---|---|
Succeeded |
true if the operation completed successfully |
Failed |
true if the operation failed (inverse of Succeeded) |
ErrorMessage |
The error message if failed, null if succeeded |
Value |
(Generic only) The return value if succeeded, throws InvalidOperationException if failed |
var r1 = Try.Run(() => ValidateName(name));
var r2 = Try.Run(() => ValidateEmail(email));
var r3 = Try.Run(() => ValidateAge(age));
// Flatten into a single result
var combined = Result.Flatten(r1, r2, r3);
if (combined.Failed)
{
// ErrorMessage contains all failures separated by newlines
Console.WriteLine(combined.ErrorMessage);
}
// Or check individually
if (Result.AnyFailed(r1, r2, r3))
{
Console.WriteLine("At least one validation failed.");
}Wrap database calls to get a clean Result instead of scattered try/catch:
// On .NET 5+ Try.RunAsync<T> returns Result<T?> (the nullable-context signature),
// so this method's return type is Result<Customer?>. On net462 / netstandard2.0
// it's Result<Customer> instead. Callers use `result.Value!` after checking
// `result.Succeeded` — the Value is non-null on the success path by contract.
public async Task<Result<Customer?>> GetCustomerByIdAsync(int id, CancellationToken token = default)
{
return await Try.RunAsync(async () =>
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(token);
await using var command = new SqlCommand("SELECT Id, Name, Email FROM Customers WHERE Id = @Id", connection);
command.Parameters.AddWithValue("@Id", id);
await using var reader = await command.ExecuteReaderAsync(token);
if (!await reader.ReadAsync(token))
throw new InvalidOperationException($"Customer {id} not found.");
return new Customer
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Email = reader.GetString(2)
};
}, token);
}
// Usage
var result = await GetCustomerByIdAsync(42);
if (result.Succeeded)
{
Console.WriteLine($"Found: {result.Value!.Name}");
}
else
{
Console.WriteLine($"Lookup failed: {result.ErrorMessage}");
}// Same nullable-context signature — Result<List<Order>?> on .NET 5+.
public async Task<Result<List<Order>?>> GetRecentOrdersAsync(int customerId, CancellationToken token = default)
{
return await Try.RunAsync(async () =>
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(token);
// ... query and return orders
return orders;
}, token);
}Result and Result<T> work well as return types from repositories and service layers. You don't need Try.Run() to create them -- use the static factory methods directly:
// On .NET 5+ Result<T>.Success / Failure return Result<T?>; declare
// the method return type accordingly. On net462 / netstandard2.0
// the same code compiles with Result<Customer>.
public class CustomerRepository
{
public Result<Customer?> GetById(int id)
{
var customer = dbContext.Customers.Find(id);
return customer is not null
? Result<Customer>.Success(customer)
: Result<Customer>.Failure($"Customer with ID {id} not found.");
}
public Result Save(Customer customer)
{
if (string.IsNullOrWhiteSpace(customer.Name))
return Result.Failure("Customer name is required.");
if (string.IsNullOrWhiteSpace(customer.Email))
return Result.Failure("Customer email is required.");
dbContext.Customers.Update(customer);
dbContext.SaveChanges();
return Result.Success();
}
public Result Delete(int id)
{
var customer = dbContext.Customers.Find(id);
if (customer is null)
return Result.Failure($"Customer with ID {id} not found.");
dbContext.Customers.Remove(customer);
dbContext.SaveChanges();
return Result.Success();
}
}Return Result from your service layer and map it to HTTP responses:
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
private readonly CustomerRepository _repository;
public CustomersController(CustomerRepository repository) => _repository = repository;
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var result = _repository.GetById(id);
return result.Succeeded
? Ok(result.Value!)
: NotFound(new { error = result.ErrorMessage });
}
[HttpPut("{id}")]
public IActionResult Update(int id, CustomerDto dto)
{
var lookup = _repository.GetById(id);
if (lookup.Failed)
return NotFound(new { error = lookup.ErrorMessage });
lookup.Value!.Name = dto.Name;
lookup.Value!.Email = dto.Email;
var saveResult = _repository.Save(lookup.Value!);
return saveResult.Succeeded
? NoContent()
: BadRequest(new { error = saveResult.ErrorMessage });
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
var result = _repository.Delete(id);
return result.Succeeded
? NoContent()
: NotFound(new { error = result.ErrorMessage });
}
}// Result<Order?> on .NET 5+, Result<Order> on legacy TFMs — see the
// database example above for the nullable-context rationale.
public Result<Order?> PlaceOrder(OrderRequest request)
{
// Validate
var validation = Result.Flatten(
ValidateCustomer(request.CustomerId),
ValidateItems(request.Items),
ValidatePayment(request.PaymentMethod)
);
if (validation.Failed)
return Result<Order>.Failure(validation.ErrorMessage!);
// Execute
return Try.Run(() =>
{
var order = orderService.Create(request);
emailService.SendConfirmation(order);
return order;
});
}This library targets:
- .NET Framework: 4.6.2
- .NET Standard: 2.0
- .NET: 8.0, 10.0
See the NuGet package page for the authoritative per-TFM compatibility matrix.
This project enforces strict code quality standards through 8 specialized analyzers, a <TreatWarningsAsErrors>true</TreatWarningsAsErrors> Release gate, and SourceLink-embedded provenance for debugger step-into.
- Microsoft.CodeAnalysis.NetAnalyzers — Built-in .NET analyzers for correctness and performance
- Roslynator.Analyzers — Advanced refactoring and code quality rules
- AsyncFixer — Async/await best practices and anti-pattern detection
- Microsoft.VisualStudio.Threading.Analyzers — Thread safety and async patterns
- Microsoft.CodeAnalysis.BannedApiAnalyzers — Prevents usage of banned synchronous APIs (see
BannedSymbols.txt) - Meziantou.Analyzer — Comprehensive code quality rules
- SonarAnalyzer.CSharp — Industry-standard code analysis
- Microsoft.CodeAnalysis.PublicApiAnalyzers — Tracks the public API surface via
PublicAPI.Shipped.txt/PublicAPI.Unshipped.txt; surfaces additions/removals at compile time as a breaking-change review gate
# Clone the repository
git clone https://github.com/Chris-Wolfgang/Try-Pattern.git
cd Try-Pattern
# Restore and build
dotnet restore
dotnet build --configuration Release
# Run tests
dotnet test --configuration Release
# Format code
dotnet format
# Verify formatting
dotnet format --verify-no-changesEvery release attaches a reproducible-build-manifest.json listing the SHA-256 of every shipped .nupkg, .snupkg, and the lib/<tfm>/*.dll files inside them, plus the SDK version and commit SHA. Any third party can rebuild from source at the release tag and confirm the produced binaries match what NuGet.org served.
See docs/REPRODUCIBLE-BUILD.md for the guarantee we make, the step-by-step verification procedure, and how to file a reproducibility-discrepancy report.
Contributions are welcome — see CONTRIBUTING.md for the development workflow, coding conventions, and PR checklist.