Skip to content

Results

Preview Results Reviewed 2026-09-30 purview-dev/results Star on GitHub 1 aspnetcore c-sharp csharp discriminated-unions dotnet error-handling minimal-apis nuget problem-details result-type roslyn source-generator union-types zodsharp llms.txt

This guide installs the packages, models an error union, returns a result, and maps it onto an HTTP response.

  • .NET 11 SDK or later — the runtime packages target net11.0; the source generator targets netstandard2.0 so any compiler host can load it.
  • C# 15 preview — union declarations are a preview language feature, so a project that declares one needs LangVersion=preview (the repository sets it centrally).
Terminal window
dotnet add package Purview.Results
dotnet add package Purview.Results.SourceGenerator # only when you model errors as a union

Purview.Results.SourceGenerator is a Roslyn component: add it as an ordinary PackageReference and it applies to the compilation automatically. It also brings the analyzer, the CS0029 code fix, and the CA1815 suppression for opted-in unions.

A C# 15 union makes the error cases strongly typed without giving up the single TError the result needs:

using Purview.Results;
[GenerateResult]
public readonly union TenantError(TenantNotFound, TenantDisabled, TenantAlreadyExists);
public readonly record struct TenantNotFound(TenantId TenantId);
public readonly record struct TenantDisabled(TenantId TenantId);
public readonly record struct TenantAlreadyExists(TenantId TenantId);

[GenerateResult] is generated by the source generator itself, so it needs no separate reference. For every case the generator emits AsFailure<TValue>() in the union’s namespace:

public static class TenantErrorResultExtensions
{
public static Result<TValue, TenantError> AsFailure<TValue>(this TenantNotFound error) =>
Result<TValue, TenantError>.Failure(error);
// ... one overload per case type
}

See Union Errors for the modelling rules and Source Generator for the generated shape.

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

Result.Success<TValue, TError>(value) and Result.Failure<TValue, TError>(error) are the same factories without repeating the value type. Both member states are also reachable through the result’s implicit conversions, so a non-union error or a value can be returned directly:

Result<int, string> ok = 42;
Result<int, string> failed = "not a number";
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}"
);

default is uninitialized: IsInitialized, IsSuccess and IsFailure are all false, and Value, Error, Match, Map, Bind and MapError throw InvalidOperationException rather than guessing. See Core Concepts and Combinators.

Terminal window
dotnet add package Purview.Results.AspNetCore
builder.Services.AddResultsHttp(options => options
.Map<TenantNotFound>(error => TypedResults.NotFound())
.Map<TenantDisabled>(error => TypedResults.Problem(statusCode: StatusCodes.Status403Forbidden))
.Map<TenantError>(error => TypedResults.Problem(statusCode: StatusCodes.Status409Conflict)));
app.MapGet("/tenants/{id}", (string id) => GetTenant(new TenantId(id))).WithResultsHttp();

A mapping for the case type wins, the mapping for the error type covers the remaining cases, and an unmapped failure is a 500 that names the unmapped case, so a mapping gap is never silent. See ASP.NET Core Integration.

Terminal window
dotnet add package Purview.Results.ZodSharp

ZodSharp’s Validate already returns a ValidationResult<T>; ToResult turns that into a result whose error type you choose:

return TenantInputSchema
.Validate(input)
.ToResult<TenantInput, TenantError>(errors => new TenantInputInvalid(input, errors));

See ZodSharp Integration and, for ProblemDetails rendering, ZodSharp Problem Details.

Every example is a non-packable project under src/examples, built on the same Tenancy domain:

Terminal window
dotnet run --project src/examples/Examples.Basic
dotnet run --project src/examples/Examples.Zod
dotnet run --project src/examples/Examples.AspNetCore --urls http://localhost:5215
dotnet run --project src/examples/Examples.AspNetCore.Zod --urls http://localhost:5216