Purview.Results 1.0.0.1

dotnet add package Purview.Results --version 1.0.0.1
                    
NuGet\Install-Package Purview.Results -Version 1.0.0.1
                    
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="Purview.Results" Version="1.0.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Purview.Results" Version="1.0.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Purview.Results" />
                    
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 Purview.Results --version 1.0.0.1
                    
#r "nuget: Purview.Results, 1.0.0.1"
                    
#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 Purview.Results@1.0.0.1
                    
#: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=Purview.Results&version=1.0.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Purview.Results&version=1.0.0.1
                    
Install as a Cake Tool

Purview.Results

NuGet version Release

A small, dependency-light Result<TValue, TError> for .NET that lets expected failures flow through a value instead of an exception. Exceptional circumstances still throw; states you expect — not found, invalid, conflict — are values. A value-less Result<TError> covers operations that have nothing to return on success.

Installation

dotnet add package Purview.Results

Quick start

using Purview.Results;

Result<Tenant, TenantError> GetTenant(TenantId tenantId) =>
    _tenants.TryGet(tenantId, out var tenant)
        ? Result<Tenant, TenantError>.Success(tenant)
        : Result<Tenant, TenantError>.Failure(new TenantNotFound(tenantId));

var result = GetTenant(tenantId);

if (result.IsSuccess)
    Console.WriteLine(result.Value.Name);

var message = result.Match(
    tenant => $"Found {tenant.Name}",
    error => $"Could not load the tenant: {error}"
);

Both member states are also reachable through implicit conversions, so an error value or a success value can be returned directly from a method whose return type is the result:

Result<int, string> ok = 42;
Result<int, string> failed = "not a number";

API

Member Purpose
Result<TValue, TError>.Success(value) / .Failure(error) Create a result
Result.Success<TValue, TError>(value) / Result.Failure<TValue, TError>(error) Create a result without naming the value twice
IsInitialized Whether the result was created at all
IsSuccess / IsFailure Which state the result holds ([MemberNotNullWhen], so nullability flows)
Value / Error The payload; throws InvalidOperationException in the other state
Match(success, failure) Fold both states into one value
Map(map) Transform the successful value, preserving the error
Bind(bind) Chain another operation that can fail, preserving the error type
Bind(bind, mapError) Chain another operation that fails with a different error type, widening the existing error
MapError(map) Transform the error, preserving the value
Throw() Return the successful value, or throw ResultException<TError> carrying the error
ToString() Success(value), Failure(error) or Uninitialized

Result<TValue, TError> is a readonly record struct. default is uninitialized: IsInitialized, IsSuccess and IsFailure are all false, and Value, Error, Match, Map, Bind and MapError throw InvalidOperationException rather than silently treating the result as a failure or a success.

IResultValue exposes a non-generic, read-only view (IsInitialized, IsSuccess, SuccessValue, ErrorValue) for infrastructure that cannot be generic over the value and error types — an ASP.NET Core endpoint filter, for example. Its accessors never throw: the one that does not describe the current state returns null.

Throw() is the deliberate escape hatch: it returns the successful value and throws a ResultException<TError> carrying the error on failure, so a boundary that must throw does not have to branch. The exception exposes the error with its original type through Error; a non-generic ResultException base carries it as object? for a catch-all. An uninitialized result is still a bug, so Throw() reports default with InvalidOperationException.

Unit results

An operation that has nothing to return on success — a command, a validation-only step — uses the value-less Result<TError>. It has the same three states and the same throw-on-misuse contract, but its success holds the Success marker rather than a value, so its success callbacks take no argument:

Result<TenantError> DeleteTenant(TenantId tenantId) =>
    _tenants.Remove(tenantId)
        ? Result<TenantError>.Success()
        : new TenantNotFound(tenantId).AsFailure();

var deleted = DeleteTenant(tenantId);

deleted.Match(() => "deleted", error => $"could not delete: {error}");   // success takes no value
deleted.Map(() => 1);                                                    // attach a value to a success
deleted.Bind(() => GetTenant(tenantId));                                 // continue with another operation
deleted.Ensure(() => _tenants.Count > 0, () => new TenantError());       // turn a success into a failure
Member Purpose
Result<TError>.Success() / .Failure(error) Create a unit result
Result.Success<TError>() / Result.Failure<TError>(error) Create a unit result without naming the error twice
IsInitialized / IsSuccess / IsFailure The three states, shared with Result<TValue, TError>
Error The error; throws InvalidOperationException in the other state
Match(success, failure) / Map(map) / Bind(bind) / MapError(map) Fold or transform; the success callbacks take no value
Throw() Return the Success marker, or throw ResultException<TError> carrying the error
ToString() Success, Failure(error) or Uninitialized
Result<TValue, TError>.DiscardValue() Turn a value result into a unit result, preserving the error
Result<TError>.MapAsync / .BindAsync / widening Bind / Ensure / Tap / Switch / OrElse The unit-result counterparts of the value-result extensions, in ResultExtensions

The implicit conversions from Success and from TError are public contract, so a method returning Result<TError> can return either the marker or a plain error.

Union error types

A C# 15 union is a natural TError: the error cases stay strongly typed. Because C# never composes the union conversion with the result's own conversion, a case value cannot be returned directly — the source generator bundled in this package generates a per-case AsFailure<TValue>() helper for [GenerateResult] unions, plus a non-generic AsFailure() that produces a unit result:

Result<Tenant, TenantError> GetTenant(TenantId tenantId) => new TenantNotFound(tenantId).AsFailure<Tenant>();
Result<TenantError> DeleteTenant(TenantId tenantId) => new TenantNotFound(tenantId).AsFailure();

The generator also declares a C# 14 extension block on the union type itself, so the union names the error:

Result<Tenant, TenantError> GetTenant(TenantId tenantId) => TenantError.Failure<Tenant>(new TenantNotFound(tenantId));
Result<TenantError> DeleteTenant(TenantId tenantId) => TenantError.Failure(new TenantNotFound(tenantId));
Result<TenantError> ok = TenantError.Success();
Result<Tenant, TenantError> created = TenantError.Success(tenant);

Use the union-receiver factory when a leaf case type is shared with another union: the per-case AsFailure helper is generated once (for the union that sorts first) and reported as RSG1006, so it can bind to the wrong union, while Union.Failure(...) always names the union you want. A case type that is itself a union is an included union: sharing it is composition, so it is not reported, and the factory also covers the included union's cases by building the nested value (RegisterTenantError.Failure<Tenant>(new BillingAccountMissing(id))).

The generator cannot declare that conversion for you: C# forbids user-defined operators in a static class, forbids conversion operators in extension members, and permits only one user-defined conversion per conversion sequence. The same package ships a code fix that offers the rewrite in the IDE when a case value is returned where a result is expected.

If you prefer to avoid the helper entirely, the cast form needs no generated code, because the cast closes the case → union conversion so only the library's union → result conversion remains:

Result<Tenant, TenantError> GetTenant(TenantId tenantId) => (TenantError)new TenantNotFound(tenantId);

When one service calls another, widen the callee's error union into the caller's operation-family union with the widening Bind, or lift it at a guard with the generated helper. List the callee's union as a case rather than its leaf cases: the caller's factory then covers the callee's cases by constructing the nested value, and a leaf is never shared. If a leaf case type must be shared, convert it with the union-receiver Union.Failure(...) factory, which is generated for every union. A case reachable through two different included unions is reported as RSG1008 and skipped.

Examples

src/src/Examples.Basic is a runnable console example over the Tenant* domain this README documents: the three result states, the combinators, probing, the throw-on-misuse contract, the generated AsFailure<TValue>() helper, chaining two services by widening one error union into another, and Throw().

dotnet run --project src/src/Examples.Basic

The repository README lists the ZodSharp, ASP.NET Core, ASP.NET Core + Zod and value-objects composition examples too.

Package Purpose
Purview.Results.AspNetCore Maps results onto ASP.NET Core responses, including ProblemDetails
Purview.Results.ZodSharp Bridges ZodSharp ValidationResult<T> into results
Purview.Results.ZodSharp.AspNetCore Maps validation-carrying failures onto HttpValidationProblemDetails

Agent skills

This package ships the purview-results-core and purview-results-union-errors agent skills, the purview-results-union-author agent and the migrate-error-returns-to-result-unions prompt under .agents/. Repositories that import Purview.BuildSdk get them mirrored into their own .agents/ folder on the next restore or build, so AI agents working there receive the guidance automatically.

License

MIT — see LICENSE.md.

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

    • No dependencies.
  • net11.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Purview.Results:

Package Downloads
Purview.Results.AspNetCore

Maps Purview.Results results onto ASP.NET Core responses, including ProblemDetails for expected failures.

Purview.Results.ZodSharp

Bridges ZodSharp ValidationResult<T> values into Purview.Results, so validation outcomes flow through the result pipeline as ordinary error values instead of exceptions.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0.1 80 10/8/2026
1.0.0 115 10/7/2026
1.0.0-prerelease.2 85 9/30/2026
1.0.0-prerelease.1 72 9/30/2026