Skip to content

OneOf to IResult

Convert OneOf<T1,T2,...> discriminated unions to IResult in a single call β€” HTTP status codes are inferred from error type names and HttpStatusCode tags.

// In Minimal API endpoints
app.MapGet("/users/{id}", async (int id) =>
    (await _service.GetUserAsync(id)).ToIResult());

app.MapPost("/users", async (CreateUserRequest req) =>
    (await _service.CreateAsync(req)).ToPostResult());   // 201 Created on success

Setup β€” add only the arities you use:

[assembly: GenerateOneOf2ExtensionsAttribute]  // OneOf<T1,T2>.ToIResult()
[assembly: GenerateOneOf3ExtensionsAttribute]  // OneOf<T1,T2,T3>.ToIResult()
[assembly: GenerateOneOf4ExtensionsAttribute]  // OneOf<T1,T2,T3,T4>.ToIResult()
Without the corresponding attribute that arity's .ToIResult() extension is not generated.

9.4.1. OneOf<T1,T2>.ToIResult()

OneOf<NotFoundError, User> result = await _service.GetAsync(id);
return result.ToIResult();  // 404 or 200

9.4.2. OneOf<T1,T2,T3>.ToIResult()

OneOf<ValidationError, ConflictError, User> result = await _service.CreateAsync(request);
return result.ToIResult();  // 422 or 409 or 200

9.4.3. OneOf<T1,T2,T3,T4>.ToIResult()

OneOf<ValidationError, UnauthorizedError, NotFoundError, Order> result =
    await _service.GetOrderAsync(id);
return result.ToIResult();  // 422 or 401 or 404 or 200

9.4.4. HTTP Method Variants

Use typed variants for non-GET endpoints to get the correct success status:

Method Success status Typical use
.ToIResult() 200 OK GET
.ToPostResult() 201 Created POST
.ToPutResult() 200 OK PUT / PATCH
.ToDeleteResult() 204 No Content DELETE
app.MapPost("/orders",    async (req) => (await _svc.CreateAsync(req)).ToPostResult());
app.MapPut("/orders/{id}", async (id, req) => (await _svc.UpdateAsync(id, req)).ToPutResult());
app.MapDelete("/orders/{id}", async (id) => (await _svc.DeleteAsync(id)).ToDeleteResult());

9.4.5. Error β†’ HTTP Status Mapping

Status codes are resolved in order of precedence:

  1. HttpStatusCode tag set on the error object at construction (domain errors set this automatically)
  2. Type-name heuristic β€” NotFoundError β†’ 404, ValidationError β†’ 422, ConflictError β†’ 409, etc.
  3. Default β†’ 400 Bad Request
// Domain errors set HttpStatusCode at construction β€” no configuration needed
public class NotFoundError : Reason<NotFoundError>      // β†’ 404
public class ValidationError : Reason<ValidationError>  // β†’ 422
public class ConflictError : Reason<ConflictError>      // β†’ 409
public class UnauthorizedError : Reason<UnauthorizedError>  // β†’ 401
public class ForbiddenError : Reason<ForbiddenError>    // β†’ 403

// Custom error with explicit tag
public class PaymentRequiredError : Error
{
    public PaymentRequiredError() => this.WithTag(HttpStatusCode.PaymentRequired);
}