![]() |
VOOZH | about |
dotnet add package PANiXiDA.Core.ResultPattern --version 1.0.2
NuGet\Install-Package PANiXiDA.Core.ResultPattern -Version 1.0.2
<PackageReference Include="PANiXiDA.Core.ResultPattern" Version="1.0.2" />
<PackageVersion Include="PANiXiDA.Core.ResultPattern" Version="1.0.2" />Directory.Packages.props
<PackageReference Include="PANiXiDA.Core.ResultPattern" />Project file
paket add PANiXiDA.Core.ResultPattern --version 1.0.2
#r "nuget: PANiXiDA.Core.ResultPattern, 1.0.2"
#:package PANiXiDA.Core.ResultPattern@1.0.2
#addin nuget:?package=PANiXiDA.Core.ResultPattern&version=1.0.2Install as a Cake Addin
#tool nuget:?package=PANiXiDA.Core.ResultPattern&version=1.0.2Install as a Cake Tool
PANiXiDA.Core.ResultPattern is a small .NET library for explicit success and failure handling in business logic without using exceptions as the primary control-flow contract.
It is designed for .NET developers who want predictable result-based workflows, typed errors, and composable synchronous and asynchronous operation pipelines.
👁 CI
👁 NuGet
👁 NuGet downloads
👁 Target Framework
When a method in business logic can end not only with success but also with an expected failure, exceptions often become an awkward contract:
try/catch blocks;PANiXiDA.Core.ResultPattern addresses this by making operation outcomes explicit:
Result represents success or failure without a value;Result<T> represents success or failure with a value;Error provides a unified error model with type, message, and metadata;Map, Bind, BindAsync, Ensure, Tap, and Match help compose operation pipelines.This library is especially useful in:
Result and Result<T>Error and ErrorTypeThe library targets net10.0.
<ItemGroup>
<PackageReference Include="PANiXiDA.Core.ResultPattern" Version="..." />
</ItemGroup>
using PANiXiDA.Core.ResultPattern;
using PANiXiDA.Core.ResultPattern;
Result<string> GetUserName(bool exists)
{
if (!exists)
{
return Result.Failure<string>(Error.NotFound("User not found"));
}
return Result.Success("John");
}
var result = GetUserName(exists: true);
if (result.IsSuccess)
{
Console.WriteLine(result.Value);
}
using PANiXiDA.Core.ResultPattern;
var validationError = Error.Validation("Email is required");
var notFoundError = Error.NotFound("User not found");
var conflictError = Error.Conflict("Email is already in use");
var forbiddenError = Error.Forbidden("Insufficient permissions");
var fieldError = Error.Validation("Invalid email format")
.WithField("email")
.WithMetadata("attemptedValue", "not-an-email");
Result without a valueusing PANiXiDA.Core.ResultPattern;
Result DeleteUser(bool userExists)
{
if (!userExists)
{
return Result.Failure(Error.NotFound("User not found"));
}
return Result.Success();
}
Result<T> with a valueusing PANiXiDA.Core.ResultPattern;
public sealed record UserDto(Guid Id, string Email);
Result<UserDto> GetUser(Guid id, UserDto? user)
{
if (user is null)
{
return Result.Failure<UserDto>(Error.NotFound("User not found"));
}
return Result.Success(user);
}
IsSuccess / IsFailure and reading FirstErrorvar result = DeleteUser(userExists: false);
if (result.IsFailure)
{
Console.WriteLine(result.FirstError.Message);
}
Value, ValueOrDefault, and TryGetValuevar userResult = GetUser(Guid.NewGuid(), new UserDto(Guid.NewGuid(), "user@example.com"));
var value = userResult.Value;
var sameValue = userResult.ValueOrDefault;
if (userResult.TryGetValue(out var user))
{
Console.WriteLine(user.Email);
}
var failedResult = Result.Failure<UserDto>(Error.NotFound("User not found"));
var defaultValue = failedResult.ValueOrDefault;
var hasValue = failedResult.TryGetValue(out var missingUser);
Console.WriteLine(defaultValue is null); // True
Console.WriteLine(hasValue); // False
Console.WriteLine(missingUser is null); // True
Result ValidateRegistration(string email, string password)
{
var errors = new List<Error>();
if (string.IsNullOrWhiteSpace(email))
{
errors.Add(Error.Validation("Email is required").WithField("email"));
}
if (string.IsNullOrWhiteSpace(password))
{
errors.Add(Error.Validation("Password is required").WithField("password"));
}
if (errors.Count > 0)
{
return Result.Failure(errors);
}
return Result.Success();
}
Combine for joining multiple validationsvar emailValidation = ValidateEmail(email);
var passwordValidation = ValidatePassword(password);
var agreementValidation = ValidateAgreement(agreementAccepted);
var validationResult = Result.Combine(
emailValidation,
passwordValidation,
agreementValidation);
if (validationResult.IsFailure)
{
return validationResult;
}
Map for transforming a resultMap is useful when the source operation is already successful and you only need to transform the value.
Result validationResult = ValidateRegistration(email, password);
Result<Guid> requestIdResult = validationResult.Map(() => Guid.NewGuid());
public sealed record User(Guid Id, string Email);
public sealed record UserResponse(Guid Id, string Email);
Result<User> userResult = Result.Success(new User(Guid.NewGuid(), "user@example.com"));
Result<UserResponse> responseResult = userResult.Map(user =>
{
return new UserResponse(user.Id, user.Email);
});
Bind for composing steps that already return ResultBind is useful when the next step can also fail.
Result validationResult = ValidateRegistration(email, password);
Result<Guid> createUserResult = validationResult.Bind(() =>
{
return CreateUser(email, password);
});
Result<User> userResult = GetUserById(userId);
Result activationResult = userResult.Bind(ActivateUser);
Result<User> userResult = GetUserById(userId);
Result<UserResponse> responseResult = userResult.Bind(user =>
{
return LoadProfile(user.Id).Map(profile =>
{
return new UserResponse(user.Id, user.Email);
});
});
BindAsync for asynchronous compositionResult validationResult = ValidateRegistration(email, password);
Result<Guid> createUserResult = await validationResult.BindAsync(() =>
{
return CreateUserAsync(email, password);
});
Result<User> userResult = await GetUserByIdAsync(userId);
Result<UserResponse> responseResult = await userResult.BindAsync(async user =>
{
var profileResult = await LoadProfileAsync(user.Id);
return profileResult.Map(profile =>
{
return new UserResponse(user.Id, user.Email);
});
});
Ensure for additional checks after successResult<User> userResult = GetUserById(userId);
Result<User> activeUserResult = userResult
.Ensure(
user => user.IsActive,
Error.Forbidden("User is blocked"))
.Ensure(
user => user.EmailConfirmed,
Error.Validation("Email is not confirmed").WithField("email"));
Tap for side effectsTap does not change the result and is useful for logging, auditing, metrics, and other side effects.
Result<User> createResult = CreateUser(email, password);
Result<User> sameResult = createResult.Tap(user =>
{
Console.WriteLine($"User created: {user.Id}");
});
Match for finishing the pipelineMatch is convenient at the application boundary, when you need to choose the final behavior for success and failure.
Result<UserResponse> result = GetUserById(userId)
.Map(user =>
{
return new UserResponse(user.Id, user.Email);
});
var response = result.Match(
onSuccess: user =>
{
return $"200 OK: {user.Email}";
},
onFailure: errors =>
{
return $"400/404: {string.Join("; ", errors.Select(error => error.Message))}";
});
Result deleteResult = DeleteUser(userExists: false);
var message = deleteResult.Match(
onSuccess: () =>
{
return "User deleted";
},
onFailure: errors =>
{
return $"Deletion failed: {errors[0].Message}";
});
using PANiXiDA.Core.ResultPattern;
public sealed record RegisterUserCommand(string Email, string Password);
public sealed record User(Guid Id, string Email, bool IsActive, bool EmailConfirmed);
public sealed record UserResponse(Guid Id, string Email);
public async Task<Result<UserResponse>> RegisterAsync(RegisterUserCommand command)
{
var validationResult = ValidateRegistration(command.Email, command.Password);
var uniqueEmailResult = validationResult.Bind(() =>
{
return EnsureEmailIsUnique(command.Email);
});
if (uniqueEmailResult.IsFailure)
{
return Result.Failure<UserResponse>(uniqueEmailResult.Errors);
}
var createResult = await uniqueEmailResult.BindAsync(() =>
{
return CreateUserAsync(command);
});
var guardedResult = createResult
.Ensure(user => user.IsActive, Error.Failure("User was created in an inconsistent state"))
.Ensure(user => user.EmailConfirmed, Error.Validation("Email is not confirmed").WithField("email"))
.Tap(user =>
{
Console.WriteLine($"Created user {user.Id}");
});
return guardedResult.Map(user =>
{
return new UserResponse(user.Id, user.Email);
});
}
public async Task<IResult> Register(RegisterUserCommand command)
{
var result = await RegisterAsync(command);
return result.Match<IResult>(
onSuccess: user =>
{
return Results.Ok(user);
},
onFailure: errors =>
{
var firstError = errors[0];
return firstError.Type switch
{
ErrorType.Validation => Results.BadRequest(errors),
ErrorType.NotFound => Results.NotFound(errors),
ErrorType.Conflict => Results.Conflict(errors),
ErrorType.Unauthorized => Results.Unauthorized(),
ErrorType.Forbidden => Results.StatusCode(StatusCodes.Status403Forbidden),
_ => Results.StatusCode(StatusCodes.Status500InternalServerError)
};
});
}
This library does not require runtime configuration.
There are no required:
appsettings.json entries;The only consumer-side requirement is referencing the package from a compatible .NET project.
.
├── src/
│ └── PANiXiDA.Core.ResultPattern/
│ └── PANiXiDA.Core.ResultPattern.csproj
├── tests/
│ └── PANiXiDA.Core.ResultPattern.UnitTests/
│ └── PANiXiDA.Core.ResultPattern.UnitTests.csproj
├── .editorconfig
├── .gitattributes
├── .gitignore
├── Directory.Build.props
├── Directory.Build.targets
├── Directory.Packages.props
├── global.json
├── version.json
├── LICENSE
└── README.md
src/ — library source codetests/ — automated testsDirectory.Build.props — shared MSBuild settingsDirectory.Build.targets — shared package metadata and package content settingsDirectory.Packages.props — centralized package versionsglobal.json — SDK and test runner configurationversion.json — Nerdbank.GitVersioning configuration.editorconfig — code style rulesREADME.md — package overview and usage documentationdotnet restore
dotnet build --configuration Release
dotnet format
dotnet test --configuration Release
dotnet restore
dotnet format
dotnet build --configuration Release
dotnet test --configuration Release
This repository uses:
Error — immutable error model with Message, Type, and Metadata
ErrorType — supported error categories:
ValidationNotFoundConflictUnauthorizedForbiddenFailureUnexpectedResult — success or failure without a value
Result<T> — success or failure with a value
Result.Success()Result.Success<T>(value)Result.Failure(...)Result.Combine(...)Map(...)Bind(...)BindAsync(...)Ensure(...)Tap(...)Match(...)Factory methods:
Error.Validation(message)Error.NotFound(message)Error.Conflict(message)Error.Unauthorized(message)Error.Forbidden(message)Error.Failure(message)Error.Unexpected(message)Additional helpers:
WithMetadata(key, value)WithField(field)Result<T>Value — returns the value on success, otherwise throws InvalidOperationExceptionValueOrDefault — returns the value on success, or default on failureTryGetValue(out value) — safely attempts to get the valueResultErrors — returns the list of errorsFirstError — returns the first error, otherwise throws InvalidOperationExceptionIsSuccess / IsFailure — explicit result state checksValue throws InvalidOperationException when the result is a failure.FirstError throws InvalidOperationException when the result is successful.Combine aggregates errors from all failed results.Match is intended for finishing a result pipeline at the application boundary.Potential future improvements:
Contributions are welcome if they keep the package focused and predictable.
.editorconfig;using Xunit; or using FluentAssertions; in test files, because they are provided as global usings in the test project;DisplayName values in English;Before considering work complete, run:
dotnet restore
dotnet format
dotnet build --configuration Release
dotnet test --configuration Release
This project is licensed under the Apache License, Version 2.0.
See the file for details.
Maintained by the PANiXiDA.
Repository:
PANiXiDA-Dotnet-Core/result-patternFor questions or improvements, use:
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 2 NuGet packages that depend on PANiXiDA.Core.ResultPattern:
| Package | Downloads |
|---|---|
|
PANiXiDA.Core.Application
Core application-layer abstractions and building blocks for .NET applications, including contracts, messaging, validation, and use case orchestration. |
|
|
PANiXiDA.Core.Presentation.Http
Reusable HTTP presentation layer abstractions, endpoint conventions, and ASP.NET Core integration utilities for PANiXiDA applications. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.10-g3b7188c478 | 94 | 4/18/2026 |
| 1.0.2 | 317 | 4/18/2026 |
| 1.0.1 | 97 | 4/18/2026 |