Resulz 2.0.0

dotnet add package Resulz --version 2.0.0
                    
NuGet\Install-Package Resulz -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Resulz" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Resulz" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Resulz" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Resulz --version 2.0.0
                    
#r "nuget: Resulz, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Resulz@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Resulz&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Resulz&version=2.0.0
                    
Install as a Cake Tool

Resulz

Resulz Build NuGet

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.UnitTest directory contains the Resulz.UnitTest.csproj project; 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 GeneratePackageOnBuild enabled, so a Release build produces .nupkg and .snupkg files in each bin/Release directory.
  • The CI workflow (.github/workflows/resultz_build.yml) builds and tests on master. 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:

  1. On nuget.org, create a trusted publishing policy for this repository: owner dogcane, repository Resulz, workflow file resultz_build.yml.
  2. Add a repository secret NUGET_USER containing the nuget.org username (profile name), not the e-mail address.
  3. Publish a GitHub release; the deploy job requests a short-lived API key via NuGet/login@v1 and pushes the packages with it.

License

GPL-3.0-only. See LICENSE.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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