ResultPattern.Net
1.0.0
dotnet add package ResultPattern.Net --version 1.0.0
NuGet\Install-Package ResultPattern.Net -Version 1.0.0
<PackageReference Include="ResultPattern.Net" Version="1.0.0" />
<PackageVersion Include="ResultPattern.Net" Version="1.0.0" />
<PackageReference Include="ResultPattern.Net" />
paket add ResultPattern.Net --version 1.0.0
#r "nuget: ResultPattern.Net, 1.0.0"
#:package ResultPattern.Net@1.0.0
#addin nuget:?package=ResultPattern.Net&version=1.0.0
#tool nuget:?package=ResultPattern.Net&version=1.0.0
Result Pattern
Result<T> represents the outcome of an operation: success with a value or failure with an error.
It is used to model failures that belong to the normal flow of an application:
- Failures that are part of the application's normal flow.
- Validation.
- Resources not found.
- Data reading.
- Conflicts.
- Permissions.
- Persistence.
- Invalid responses or expected errors from external services.
- Flows where each step depends on the previous one.
It is useful in situations where an operation can fail normally or when failure is part of the expected behavior.
It does not replace exceptions, it complements them.
Why the Result Pattern?
The Result pattern provides a clear and explicit way to manage success and failure without relying on exceptions for control flow.
Instead of throwing exceptions or returning null, methods return a structured result that makes outcomes predictable and easier to handle.
❌ Problems it solves
Traditional approaches often lead to:
- Hidden control flow through exceptions
nullvalues and potentialNullReferenceExceptions- Deeply nested
ifstatements - Inconsistent error handling across the application
✔️ Benefits
With Result<T> you get:
- Explicit success/failure flow
- Non-exception-based control flow
- Safer and more predictable APIs
- Easier composition of operations
- Improved testability
Result type
The fundamental usage of Result is by explicitly returning either success or failure.
Result (without a value) is used for operations where only the success or failure of the operation matters.
public Result SaveUser(User user)
{
if (user is null)
return Result.Failure(Error.Validation("User.Required", "User is required"));
_users.Save(user);
return Result.Ok();
}
Result<T> is used when an operation needs to return a value.
public Result<User> CreateUser(string name)
{
if (string.IsNullOrWhiteSpace(name))
return Result<User>.Failure(Error.Validation("User.NotFound", "User is required"));
return Result<User>.Ok(new User(name));
}
The result can be returned either explicitly or implicitly through implicit conversion
Properties
Result<T> exposes the following properties to inspect the result state:
IsSuccess: Indicates whether the operation succeeded.IsFailure: Indicates whether the operation failed. This is the inverse ofIsSuccess.Error: Contains the error associated with a failure. If the result is successful, it containsError.None.Value: Contains the result value when the operation succeeds. If the result is a failure, accessing this property throws an exception.
Check IsSuccess or IsFailure when the flow uses 'early returns'.
Result<User> result = await GetUserAsync(id);
if (result.IsFailure)
{
Console.WriteLine($"Error: {result.Error.Message}");
return;
}
User user = result.Value;
Console.WriteLine($"User found: {user.Name}");
Value should only be accessed after confirming that the result is successful. Otherwise, an exception will be thrown. This is the expected behavior.
Error type
Error describes an expected failure in a stable and structured way.
public sealed record Error(ErrorType Type, string Code, string Message);
| Property | Type | Description
| ---------|-------|--------------------------|
| Type | ErrorType | Categorizes the error at a high level. Makes it easier to identify the nature of the failure.
|Code | string | A domain-specific error identifier. It should remain stable and be useful for tracing, logging, testing, and integrations.
|Message | string | A human-readable description of the problem. It may vary depending on the context.
<details>
<summary>View ErrorType enum</summary>
public enum ErrorType
{
// Success
None,
// Input / Permissions
Validation, // Invalid input data
Unauthorized, // Not authenticated / invalid credentials
Forbidden, // Insufficient permissions
// State / Resources
NotFound, // Resource does not exist
Conflict, // Inconsistent state (e.g. duplicate, concurrency)
InvalidOperation, // Invalid state for performing the action
NotSupported, // Operation not supported in this context
// Execution
Timeout, // Operation exceeded the time limit
Cancelled, // Operation was cancelled (cancellation token, user, etc.)
// Other
General, // Unspecified or unknown error
Internal, // Internal technical error (bug, unhandled exception)
}
</details> <br>
The Error type provides static factory methods for representing the most common system errors.
You can use the built-in factories to create errors with a consistent format:
Error.Validation("User.EmailInvalid", "The email address is invalid.");
Error.NotFound("User.NotFound", $"The user with id '{id}' does not exist.");
Error.Conflict("User.AlreadyExists", "The user is already registered.");
Error.Unauthorized("Auth.InvalidCredentials", "The credentials are invalid.");
Error.Internal("Failed to read configuration.");
Error.General();
Implicit Conversion
Result<T> supports implicit conversions to reduce boilerplate and enable a more natural expression of the flow [docs].
This allows you to return either an Error or a value of type T directly, without manually wrapping them in a Result.
Error → Result / Result<T>
T → Result<T>
This reduces boilerplate and makes failure paths more concise, improving readability in methods that return Result<T>.
public async Task<Result<User>> GetUserAsync(int id)
{
var user = await _repo.FindAsync(id);
return user is null
? Error.NotFound("User.NotFound", $"The user with id '{id}' does not exist")
: user;
}
In the example above:
Error → Result<User>through implicit conversionUser → Result<User>through implicit conversion
Extensions
Extensions are available in both synchronous and asynchronous versions.
The asynchronous version is where the pattern provides the most value, allowing you to chain validations, repositories, external services, and transformations without multiple nested if statements. It operates on Task<Result<T>> and is the most common choice in modern applications, where I/O operations and result composition are prevalent.
| Function | Purpose |
|---|---|
Map / MapAsync |
Transform |
Bind / BindAsync |
Chain |
Ensure / EnsureAsync |
Validate |
OnSuccess / OnSuccessAsync |
Execute on success |
OnFailure / OnFailureAsync |
Execute on failure |
Match / MatchAsync |
Close (returns a value) |
Switch / SwitchAsync |
Close (executes an action) |
Match / MatchAsync
Use Match or MatchAsync to return a value by handling both outcomes (success or failure). Define what should happen when the operation succeeds and what should happen when it fails.
Both delegates must return the same type.
string message = await GetUserAsync(id)
.MatchAsync(
user => $"User: {user.Name}",
error => $"Error: {error.Message}");
Switch / SwitchAsync
Use Switch or SwitchAsync to terminate the flow and execute an action. It does not return a value.
await GetUserAsync(id)
.SwitchAsync(
user => Console.WriteLine($"Current user: {user.Name}"),
error => Console.WriteLine(error.Message));
Map / MapAsync
Map and MapAsync are used to transform the value contained in a successful Result<T>.
If the result is a failure, the error is propagated without applying the transformation.
// Async: User -> UserDto if GetUserAsync succeeds
Result<UserDto> asyncResult = await GetUserAsync(id)
.MapAsync(user => new UserDto(user.Id, user.Name));
// Sync: synchronous version
Result<UserDto> syncResult = GetUser(id)
.Map(user => new UserDto(user.Id, user.Name));
Bind / BindAsync
Bind and BindAsync are used to chain multiple dependent operations. Use them when the next step in the chain also returns a Result<T> and can therefore fail. They allow complex flows to be expressed linearly, without nesting.
If any operation fails, the error is automatically propagated, returned, and execution stops.
// Chaining with another operation that returns Result
Result<Profile> asyncResult = await GetUserAsync(id)
.BindAsync(user => GetProfileAsync(user.Id));
// Multiple dependent operations
Result<Order> result = await ValidateRequestAsync(request)
.BindAsync(validRequest => CreateOrderAsync(validRequest))
.BindAsync(order => ReserveStockAsync(order))
.BindAsync(order => SaveOrderAsync(order));
The flow reads from top to bottom as a sequence of domain steps.
This approach avoids callback hell and deeply nested if statements, keeping the execution flow more declarative and easier to read.
Ensure / EnsureAsync
Use Ensure or EnsureAsync to validate the value of a successful Result without leaving the flow.
// Async
Result<User> asyncResult = await GetUserAsync(id)
.EnsureAsync(
user => user.IsActive,
Error.Validation("User.State", "The user is not active."));
// Sync
Result<User> syncResult = GetUser(id)
.Ensure(
user => user.IsActive,
Error.Validation("User.State", "The user is not active."));
If the result is already a failure, the validation is not executed and the original error is preserved.
OnSuccess / OnSuccessAsync
Use OnSuccess or OnSuccessAsync to execute a side effect only when the Result is successful, without modifying the value or interrupting the flow.
They are useful for logging, metrics, caching, notifications, and similar scenarios.
// Async
Result<User> asyncResult = await GetUserAsync(id)
.OnSuccessAsync(user => Log($"User found: {user.Id}"));
// Sync
Result<User> syncResult = GetUser(id)
.OnSuccess(user => Log($"User found: {user.Id}"));
OnSuccess is useful when you need to observe the flow without affecting its outcome.
OnFailure / OnFailureAsync
Use OnFailure or OnFailureAsync to execute a side effect (logging, metrics, alerts, etc.) only when the operation fails, without modifying the result or interrupting the flow.
// Async
Result<User> asyncResult = await GetUserAsync(id)
.OnFailureAsync(error => Log($"Error: {error.Message}"));
// Sync
Result<User> syncResult = GetUser(id)
.OnFailure(error => Log($"Error: {error.Message}"));
It does not transform the error; it only observes the failure and returns the same result, allowing the chain to continue.
Complete Example
public async Task<Result<UserDto>> GetUserDtoAsync(int id)
{
return await GetUserAsync(id)
.EnsureAsync(
user => user.IsActive,
Error.Validation("User.State", "The user is not active."))
.BindAsync(user => LoadPermissionsAsync)
.OnSuccessAsync(user => Log($"User loaded: {user.Id}"))
.OnFailureAsync(error => Log($"Error: {error.Message}"))
.MapAsync(UserToDto);
}
private static UserDto UserToDto(User user) =>
new(user.Id, user.Name);
Consuming the result:
public async Task<string> GetUserMessageAsync(int id)
{
var result = await _userService.GetUserDtoAsync(id);
return result.Match(
dto => $"User: {dto.Name}",
error => $"Error: {error.Message}");
}
This flow represents a declarative composition where each step operates on the result of the previous one without breaking the chain.
If a step fails, the following steps are not executed. The error is propagated to the end of the flow.
The synchronous flow is best suited for pure validations, in-memory transformations, or domain rules that do not require I/O.
public Result<int> CalculateTotal(int price)
{
return Ok(price)
.Ensure(value => value > 0,
Error.Validation("Value.Invalid", "The value must be greater than 0"))
.Map(value => value * 2)
.OnSuccess(value => Log($"Calculated total: {value}"));
}
Usage Guidelines
🟢 Best Practices
- Use Result<T> for expected use-case failures.
- Return an Error as soon as a validation fails.
- Prefer async flows when working with I/O, external services, or repositories.
- Use Map when you only need to transform the value.
- Use Bind when the next step returns a Result.
- Use Ensure for validations within the chain.
- Use OnSuccess and OnFailure for side effects (logging, metrics, etc.).
- End flows with Match, IsFailure, or IsSuccess.
- Keep Code values stable; Message values may change.
- If there is no valid value, return an Error instead of null.
🔴 Anti-Patterns
- Do not use Result<T> to hide programming errors.
- Do not access Value without checking for success first.
- Do not use OnSuccess or OnFailure to modify the result.
- Do not return null as a successful result.
- Do not mix domain logic and error handling within the same flow.
Use Result<T> to control the flow, not to hide it.
Contributing
Contributions are welcome! Feel free to open an issue or submit a pull request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2026 BracoZS
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 86 | 5/31/2026 |