Documentation
adrs/014-typedresults-type-safe-responses.md
ADR-014: TypedResults for Type-Safe Responses
Status
Accepted
Context
Minimal APIs in .NET can return various types from endpoint handlers, including raw objects, IResult implementations, or custom types. Without a consistent approach, APIs can become inconsistent in their response types and status codes. Additionally, OpenAPI documentation generation works better with strongly-typed responses.
Key challenges:
- Inconsistent response types across endpoints
- Lack of compile-time safety for HTTP status codes
- Poor OpenAPI/Swagger documentation without explicit types
- Difficulty in testing without known response types
- Implicit status code determination can be confusing
Decision
We will use TypedResults and the Results<T1, T2, ...> pattern for all minimal API endpoint responses to ensure type safety and consistent HTTP responses.
Pattern:
private static Results<Ok<Product>, NotFound, BadRequest<string>> GetProduct(
[FromRoute] int id,
[FromServices] IProductService service)
{
// Return type explicitly declares all possible outcomes
if (id <= 0)
return TypedResults.BadRequest("Invalid product ID");
var product = service.GetById(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
}
Consequences
Positive
- Type Safety: Compile-time checking of all response types
- OpenAPI Generation: Automatic, accurate API documentation
- Consistency: Standardized response patterns across all endpoints
- IntelliSense: IDE support for available response types
- Testing: Known response types make testing easier
- Self-Documenting: Return signature shows all possible responses
Negative
- Verbosity: Return types can become long with multiple outcomes
- Learning Curve: Developers must understand Results<> pattern
- Limited Results: Maximum of 6 result types in Results<>
- Boilerplate: More explicit code compared to returning raw objects
Neutral
- Explicit Status Codes: Must explicitly specify HTTP status codes
- No Implicit Conversion: Cannot return raw objects directly
- Pattern Matching: Often used with pattern matching for handling
Implementation Notes
Common Response Patterns
Single Success Response
private static Ok<List<Product>> GetAllProducts(
[FromServices] IProductService service)
{
var products = service.GetAll();
return TypedResults.Ok(products);
}
Success or Not Found
private static Results<Ok<Product>, NotFound> GetProductById(
[FromRoute] int id,
[FromServices] IProductService service)
{
var product = service.GetById(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
}
Create with Validation
private static async Task<Results<Created<Product>, ValidationProblem>> CreateProduct(
[FromBody] CreateProductRequest request,
[FromServices] IValidator<CreateProductRequest> validator,
[FromServices] IProductService service)
{
var validation = await validator.ValidateAsync(request);
if (!validation.IsValid)
return TypedResults.ValidationProblem(validation.ToDictionary());
var product = await service.CreateAsync(request);
return TypedResults.Created($"/api/products/{product.Id}", product);
}
Multiple Error Scenarios
private static async Task<Results<Ok<Product>, NotFound, Conflict, BadRequest<string>>> UpdateProduct(
[FromRoute] int id,
[FromBody] UpdateProductRequest request,
[FromServices] IProductService service)
{
if (id != request.Id)
return TypedResults.BadRequest("ID mismatch");
var existing = await service.GetByIdAsync(id);
if (existing is null)
return TypedResults.NotFound();
if (await service.IsDuplicateName(request.Name, id))
return TypedResults.Conflict();
var updated = await service.UpdateAsync(request);
return TypedResults.Ok(updated);
}
TypedResults Factory Methods
Common TypedResults methods:
// 2xx Success
TypedResults.Ok(value) // 200
TypedResults.Created(uri, value) // 201
TypedResults.Accepted(value) // 202
TypedResults.NoContent() // 204
// 4xx Client Errors
TypedResults.BadRequest(error) // 400
TypedResults.Unauthorized() // 401
TypedResults.Forbidden() // 403
TypedResults.NotFound() // 404
TypedResults.Conflict() // 409
TypedResults.UnprocessableEntity(error) // 422
TypedResults.ValidationProblem(errors) // 400 with validation
// 5xx Server Errors
TypedResults.InternalServerError() // 500 (avoid using)
TypedResults.StatusCode(500) // Generic status code
OpenAPI Documentation
TypedResults automatically generates accurate OpenAPI specs:
/api/products/{id}:
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'404':
description: Not Found
'400':
content:
text/plain:
schema:
type: string
Testing with TypedResults
[Test]
public async Task GetProduct_WhenExists_ReturnsOk()
{
// Arrange
var service = Substitute.For<IProductService>();
service.GetById(1).Returns(new Product { Id = 1 });
// Act
var result = GetProduct(1, service);
// Assert
result.Result.Should().BeOfType<Ok<Product>>();
var okResult = (Ok<Product>)result.Result;
okResult.Value.Id.Should().Be(1);
}
Error Response Consistency
Standardize error responses:
public record ProblemDetails(string Title, string Detail, int Status);
private static BadRequest<ProblemDetails> BadRequestProblem(string detail)
{
return TypedResults.BadRequest(new ProblemDetails(
Title: "Bad Request",
Detail: detail,
Status: 400
));
}
Handling File Responses
private static Results<FileContentHttpResult, NotFound> DownloadFile(
[FromRoute] int id,
[FromServices] IFileService service)
{
var file = service.GetFile(id);
return file is not null
? TypedResults.File(file.Content, file.ContentType, file.Name)
: TypedResults.NotFound();
}
Best Practices
- Always use TypedResults factory methods instead of creating IResult manually
- Order result types by likelihood: Most common first
- Limit to 3-4 result types when possible for readability
- Use ValidationProblem for validation errors with details
- Avoid InternalServerError: Let exception middleware handle 500s
- Be specific with error types: BadRequest
vs BadRequest
Related ADRs
- ADR-013: Minimal APIs with Static Classes (endpoint structure)
- ADR-017: OpenAPI Metadata Enrichment (documentation generation)
- ADR-021: Microsoft Kiota for API Client Generation (client generation from types)