Skip to content

Results

Preview

Result for .NET — expected failures as values the caller must handle.

aspnetcore c-sharp csharp discriminated-unions dotnet error-handling minimal-apis nuget problem-details result-type roslyn source-generator union-types zodsharp

Use cases

Concrete examples of what Results does, and who each one is for.

Developer

Return expected failures as values

You call a store that can fail in ways you expect — not found, disabled, already exists — and you do not want a thrown exception or a generic error string to be the only way the caller learns what happened.

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

What you get: The caller branches on the error value itself, so TenantNotFound keeps carrying its TenantId instead of being flattened into a message.

The core package has no dependencies: it ships the result type, its factories, and IResultValue without pulling anything else in.

Read the guide →
Developer

Generate the failure helper for every union case

You model errors as a C# 15 union and need each case to become the failure side of a result without writing one conversion helper per case by hand.

csharp
[GenerateResult]
public readonly union TenantError(TenantNotFound, TenantDisabled, TenantAlreadyExists);

What you get: The generator emits AsFailure<TValue>() for every case of an annotated union, plus a code fix and a CA1815 suppression, so the declaration needs no pragma.

C# forbids the implicit conversion a bare case value would need — no operators in static classes, no conversion operators in extension members, one user-defined conversion per sequence — which is why the helper is per case.

Read the guide →
Architect

Map one error union onto HTTP responses

The same domain error union surfaces from many minimal-API endpoints, and you do not want a hand-written switch deciding the status code at each one.

csharp
builder.Services.AddResultsHttp(options => options
    .Map<TenantNotFound>(error => TypedResults.NotFound())
    .Map<TenantError>(error => TypedResults.Problem(statusCode: StatusCodes.Status409Conflict))
);

app.MapGet("/tenants/{id:int}", (int id) => GetTenant(id)).WithResultsHttp();

What you get: The mapping is declared once at startup, WithResultsHttp() applies it to the endpoints, and a host can replace IResultsHttpMapper when it needs different behaviour.

Mappings resolve in declaration order — any mapping or failure mapper the host declared earlier always wins — so the most specific rule is registered first.

Read the guide →
Team lead

Let ZodSharp validation failures become validation problems

Endpoints validate input with ZodSharp, and a failed validation should reach the client as the standard ASP.NET Core validation-problem payload rather than an error shape the API has to document separately.

json
{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": { "TenantId": ["Required field 'TenantId' is null"] },
  "issues": [{ "code": "missing_field", "path": ["TenantId"], "message": "Required field 'TenantId' is null" }]
}

What you get: A failure that implements IValidationErrorCarrier becomes HttpValidationProblemDetails without the host mapping it, unless a per-code or per-category rule answers that failure first.

The response is ASP.NET Core's own HttpValidationProblemDetails contract, so API clients keep the error shape they already parse.

Read the guide →

The problem

A small, dependency-free Result<TValue, TError> for .NET that makes expected failures part of a method's contract: not found, invalid, and conflict are values rather than exceptions. A Roslyn source generator adds C# 15 union ergonomics for the error cases, and companion packages carry the result into ASP.NET Core responses and ZodSharp validation problems.

From the repository

Result types for .NET - a small, dependency-light Result<TValue, TError> where expected failures are values instead of exceptions, C# 15 union error cases with generated AsFailure<TValue>() helpers, and ASP.NET Core and ZodSharp integrations that map failures to HTTP responses.

Why it became a project

Services kept re-declaring their own error types and hand-writing the same failure helpers and HTTP mapping, so the result type, the generated helpers, and the adapter packages became one shared package family.

Where it fits

Use it when

Expected failures are part of your domain model and you want them returned as values the caller must handle, with a strongly typed error case that still reaches the client as a standard HTTP problem response.

It may not fit when

You target a runtime below .NET 11 with C# 15 preview, or the failures you are modelling are exceptional circumstances that belong in exceptions.

Packages

5 packages

Purview.ResultsInstall ↓—1.0.0-prerelease.2—
Purview.Results.AspNetCoreInstall ↓—1.0.0-prerelease.2—
Purview.Results.SourceGeneratorInstall ↓—1.0.0-prerelease.2—
Purview.Results.ZodSharpInstall ↓—1.0.0-prerelease.2—
Purview.Results.ZodSharp.AspNetCoreInstall ↓—1.0.0-prerelease.2—

Install

Purview.Results primary

.NET CLI
dotnet add package Purview.Results

Purview.Results.SourceGenerator

.NET CLI
dotnet add package Purview.Results.SourceGenerator

Purview.Results.AspNetCore

.NET CLI
dotnet add package Purview.Results.AspNetCore

Purview.Results.ZodSharp

.NET CLI
dotnet add package Purview.Results.ZodSharp

Purview.Results.ZodSharp.AspNetCore

.NET CLI
dotnet add package Purview.Results.ZodSharp.AspNetCore