Resulz 2.0.0
dotnet add package Resulz --version 2.0.0
NuGet\Install-Package Resulz -Version 2.0.0
<PackageReference Include="Resulz" Version="2.0.0" />
<PackageVersion Include="Resulz" Version="2.0.0" />
<PackageReference Include="Resulz" />
paket add Resulz --version 2.0.0
#r "nuget: Resulz, 2.0.0"
#:package Resulz@2.0.0
#addin nuget:?package=Resulz&version=2.0.0
#tool nuget:?package=Resulz&version=2.0.0
Resulz
Resulz is a small "operation result" library with a fluent validation framework for .NET.
Instead of throwing exceptions for expected failures, return an OperationResult (or
OperationResult<T>) that carries success state, contextual error messages, and optional
additional information.
Packages
| Package | Description |
|---|---|
Resulz |
Core result types, fluent helpers, and the validation DSL. |
Resulz.Web |
ASP.NET Core integration: turns results into IActionResults. |
Resulz.Validation.Countries |
EU VAT number and VAT recipient code validation. |
All packages target .NET 10. Version 1.x targeted .NET 8; 2.x is a breaking change because of the target framework bump.
dotnet add package Resulz
dotnet add package Resulz.Web
dotnet add package Resulz.Validation.Countries
Operation results
OperationResult represents an operation without a return value; OperationResult<T>
carries a value. A result is successful when it has no errors.
using Resulz;
OperationResult success = OperationResult.MakeSuccess();
OperationResult failure = OperationResult.MakeFailure(
ErrorMessage.Create("Email", "EMAIL_REQUIRED"));
if (!failure.Success)
{
foreach (ErrorMessage error in failure.Errors)
{
Console.WriteLine($"{error.Context}: {error.Description}");
}
}
Errors can be appended, prefixed, and translated after creation:
var result = OperationResult
.MakeSuccess()
.AppendError("Email", "EMAIL_NOT_VALID")
.AppendContextPrefix("Customer.")
.SetAdditionalInfo(OperationResultStandardInfoCodes.BAD_REQUEST);
// Error context is now "Customer.Email"
AdditionalInfo is a free-form string used to carry a status code or description. Multiple
values passed to SetAdditionalInfo are joined with |; each call replaces the previous
value. Use the OperationResultStandardInfoCodes helpers instead of raw strings:
result.SetNotFoundCode(); // "NOT_FOUND"
result.SetNotAuthorizedCode(); // "NOT_AUTHORIZED"
result.SetNotValidCode(); // "NOT_VALID"
result.SetBadRequestCode(); // "BAD_REQUEST"
result.SetGenericErrorCode(); // "GENERIC_ERROR"
Results with a value
OperationResult<Customer> created = OperationResult<Customer>.MakeSuccess(customer);
if (created.Success)
{
Customer value = created.Value;
}
Implicit conversions keep fluent code concise:
OperationResult<int> ok = 42; // wraps the value as a success
OperationResult plain = ok; // drops the value
OperationResult<int> failed = failure; // carries the errors, no value
Converting a successful non-generic result to OperationResult<T> throws
ArgumentException, because there is no value to carry. This conversion exists for failure
propagation inside fluent chains.
Fluent helpers
All helpers return the original result (when successful) so they can be chained:
OperationResult result = OperationResult
.MakeSuccess()
.IfSuccess(r => Console.WriteLine("ok"))
.IfFailed(r => Console.WriteLine($"{r.Errors.Count()} error(s)"));
OperationResult saved = result.Then(() => SaveChanges());
| Helper | Behavior |
|---|---|
IfSuccess(Action<T>) |
Runs the action only on success. |
IfFailed(Action<T>) |
Runs the action only on failure. |
IfSuccessThenReturn<T>(Func<T>) |
On success, creates OperationResult<T> from the factory; on failure, propagates the errors. |
Then(Func<T>) / Then(Func<OperationResult<T>>) |
Runs the next operation only on success; otherwise propagates the failure. |
HasErrors(context, description) |
Case-insensitive match on both context and description. |
HasErrorsByContext(context) |
Case-insensitive match on context. |
HasErrorsByDescription(description) |
Case-insensitive match on description. |
OperationResult also defines true/false and & operators, so if (result) checks
Success and result1 & result2 merges the errors of both.
Validation
Validation rules are extension methods on ValueChecker<T>. Start a chain from a successful
result with With(value, context) and read the final result from .Result:
using Resulz.Validation;
var result = OperationResult
.MakeSuccess()
.With(name, nameof(name)).Required().StringLength(250)
.With(email, nameof(email)).Required().Email()
.With(age, nameof(age)).Between(18, 100)
.Result;
Each rule has two forms: a default-message overload and one that accepts a custom message.
Default descriptions follow the {CONTEXT}_{CODE} convention (uppercased), for example
NAME_REQUIRED or EMAIL_NOT_EMAIL.
| Rule | Fails when | Default description |
|---|---|---|
Required() |
Value is null, or a string is empty/whitespace. |
{CONTEXT}_REQUIRED |
StringLength(max) |
String length is greater than max. |
{CONTEXT}_TOO_LONG |
Email() |
String does not match the e-mail pattern. | {CONTEXT}_NOT_EMAIL |
StringMatch(regex[, options]) |
String does not match the regular expression. | {CONTEXT}_NOT_MATCHED |
EqualTo(value) / EqualTo(value, comparison) |
Values are not equal. | {CONTEXT}_NOT_EQUAL |
Between(min, max) |
Value is <= min or >= max (bounds are exclusive). |
{CONTEXT}_NOT_BETWEEN |
GreaterThen(value) |
Value is <= value (the API spelling is Then, not Than). |
{CONTEXT}_NOT_GREATER |
GreaterThenOrEqual(value) |
Value is < value. |
{CONTEXT}_NOT_GREATER_OR_EQUAL |
LessThen(value) |
Value is >= value. |
{CONTEXT}_NOT_LESS |
LessThenOrEqual(value) |
Value is > value. |
{CONTEXT}_NOT_LESS_OR_EQUAL |
Into(values) |
Value is not contained in the array. | {CONTEXT}_NOT_INTO |
Condition(predicate) |
Predicate returns false. |
{CONTEXT}_CONDITION_FAILED |
Custom messages are passed as the last argument:
.With(loanAmount, nameof(loanAmount))
.GreaterThen(0, "LOAN_AMOUNT_MUST_BE_POSITIVE")
By default every rule in the chain runs and can add an error. Call StopOnError() on the
checker to skip the remaining rules once the result has at least one error:
.With(name, nameof(name)).StopOnError().Required().StringLength(250)
ASP.NET Core integration
Resulz.Web converts results to action results. Add the package and use
ToActionResult():
using Resulz.Web;
[ApiController]
public class CustomersController : ControllerBase
{
[HttpPost]
public IActionResult Create(CustomerRequest request)
{
var result = Customer.Create(request.Name, request.Email);
return result.ToActionResult();
}
}
ToActionResult maps AdditionalInfo to HTTP responses:
| Result | Response |
|---|---|
Successful OperationResult |
204 No Content |
Successful OperationResult<T> |
200 OK with Value as body |
BAD_REQUEST |
400 Bad Request with the result as body |
NOT_AUTHORIZED |
401 Unauthorized (empty body) |
NOT_FOUND |
404 Not Found with the result as body |
NOT_VALID |
422 Unprocessable Entity with the result as body |
| Missing or unknown code | 500 Internal Server Error with the result as body |
Country VAT validation
Resulz.Validation.Countries validates EU VAT numbers and VAT recipient codes:
using Resulz.Validation;
using Resulz.Validation.Countries;
var result = OperationResult
.MakeSuccess()
.With(vatNumber, nameof(vatNumber)).ValidateVatNumber("IT")
.With(recipientCode, nameof(recipientCode)).ValidateVatRecipientCode("IT")
.Result;
VAT numbers may be supplied with or without the country prefix; CountrySafeVatNumber
normalizes them before validation (for example "40474690803" becomes "IT40474690803").
Supported country codes: AT, BE, BG, CY, CZ, DE, DK, EE, EL (Greece),
ES, FI, FR, GB, HR, HU, IE, IT, LT, LU, LV, MT, NL, PL, PT,
RO, SE, SI, SK. Any other code falls back to UnknownCountryContextValidator,
which rejects every VAT number and recipient code.
Default descriptions are {CONTEXT}_NOT_VATNUMBER and {CONTEXT}_NOT_VATRECIPIENTCODE.
Building and testing
Prerequisites: .NET 10 SDK.
dotnet restore
dotnet build Resulz.sln -c Release
dotnet test Resulz.sln -c Release
# run a single test
dotnet test src/Resultz.UnitTest/Resulz.UnitTest.csproj -c Release \
--filter "FullyQualifiedName~RequiredTestFailWithString"
# run the sample application
dotnet run --project src/Resulz.Samples
The sample application is interactive and waits for Enter before exiting.
The
src/Resultz.UnitTestdirectory contains theResulz.UnitTest.csprojproject; the folder name is a historical typo.
Versioning and release
- The version for all three packages is defined once in
src/Directory.Build.props(PackageVersion). Bump it there, never in individual project files. - The three library projects have
GeneratePackageOnBuildenabled, so a Release build produces.nupkgand.snupkgfiles in eachbin/Releasedirectory. - The CI workflow (
.github/workflows/resultz_build.yml) builds and tests onmaster. Publishing a GitHub release packs the three packages and publishes them to nuget.org.
Publishing uses NuGet Trusted Publishing over GitHub Actions OIDC, so no long-lived NuGet API key is stored in the repository. To configure it:
- On nuget.org, create a trusted publishing policy for this repository:
owner
dogcane, repositoryResulz, workflow fileresultz_build.yml. - Add a repository secret
NUGET_USERcontaining the nuget.org username (profile name), not the e-mail address. - Publish a GitHub release; the deploy job requests a short-lived API key via
NuGet/login@v1and pushes the packages with it.
License
GPL-3.0-only. See LICENSE.
| 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 (3)
Showing the top 3 NuGet packages that depend on Resulz:
| Package | Downloads |
|---|---|
|
TrinityText.Business
Trinity Text business layer |
|
|
Resulz.Web
Resulz integrations for AspNet |
|
|
Resulz.Validation.Countries
An "operation result" implementation with a "bit" of validation framework. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0 | 85 | 10/24/2026 |
| 1.6.1 | 924 | 7/29/2025 |
| 1.6.0 | 420 | 7/26/2025 |
| 1.5.3 | 2,372 | 2/22/2024 |
| 1.5.2 | 532 | 12/15/2023 |
| 1.5.1 | 331 | 12/15/2023 |
| 1.5.0 | 326 | 12/14/2023 |
| 1.4.1 | 624 | 11/6/2023 |
| 1.4.0 | 249 | 11/6/2023 |
| 1.3.0 | 1,015 | 7/19/2023 |
| 1.2.0 | 1,198 | 4/13/2023 |
| 1.1.0 | 627 | 8/19/2022 |
| 1.0.0 | 7,472 | 1/13/2021 |