ASP.NET Core  

Building High-Performance Minimal APIs with Typed Results in .NET Core

Minimal APIs were introduced to streamline HTTP endpoint creation by stripping away the heavy boilerplate code of traditional ControllerBase classes. However, returning dynamic types like IResult or loose objects (e.g., results.Ok(obj)) can make code hard to test and obscure contract definitions in OpenAPI/Swagger documentation.

Typed Results solve this by offering concrete implementations of IResult that implement specific HTTP status codes, drastically improving type safety, unit testability, and Swagger schema generation.

Step 1: Initialize the Minimal API Project

Create a new web application using the command-line interface (CLI):

Bash

dotnet new web -n TypedResultsApi
cd TypedResultsApi

Open Program.cs and configure it to handle simple user-management data objects.

Step 2: Define Your Data Model and In-Memory Repository

For demonstration purposes, define a simple record model and a static collection acting as a database repository.

C#

public record Product(int Id, string Name, decimal Price);

public static class ProductRepository
{
    private static readonly List<Product> _products = new()
    {
        new(1, "Mechanical Keyboard", 89.99m),
        new(2, "Ergonomic Mouse", 49.99m),
        new(3, "UltraWide Monitor", 399.99m)
    };

    public static List<Product> GetAll() => _products;
    
    public static Product? GetById(int id) => _products.FirstOrDefault(p => p.Id == id);
    
    public static void Add(Product product) => _products.Add(product);
}

Step 3: Implement Endpoints Using TypedResults

Modify Program.cs to map your endpoints using static methods returning Results<T1, T2> unions or specific typed results like Ok<T>, NotFound, and Created<T>.

C#

var builder = WebApplication.CreateBuilder(args);

// Add services for OpenAPI/Swagger documentation generation
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// 1. GET: Retrieve all products
app.MapGet("/products", () => 
    TypedResults.Ok(ProductRepository.GetAll()))
    .WithName("GetAllProducts")
    .WithOpenApi();

// 2. GET: Retrieve product by ID with Typed Results union (Ok vs NotFound)
app.MapGet("/products/{id:int}", Results<Ok<Product>, NotFound> (int id) =>
{
    var product = ProductRepository.GetById(id);
    
    if (product is null)
    {
        return TypedResults.NotFound();
    }
    
    return TypedResults.Ok(product);
})
.WithName("GetProductById")
.WithOpenApi();

// 3. POST: Create a new product with Created structural response
app.MapPost("/products", Results<Created<Product>, BadRequest<string>> (Product product) =>
{
    if (string.IsNullOrWhiteSpace(product.Name))
    {
        return TypedResults.BadRequest("Product name cannot be empty.");
    }

    ProductRepository.Add(product);
    
    // Returns a 201 Created status with location header and body payload
    return TypedResults.Created($"/products/{product.Id}", product);
})
.WithName("CreateProduct")
.WithOpenApi();

app.Run();

Step 4: Run and Verify the API

Execute your application using:

Bash

dotnet run

Navigate to https://localhost:{port}/swagger in your browser. Notice how cleanly OpenAPI documents the return types (200 OK, 404 Not Found, 400 Bad Request) because TypedResults communicates the exact response contracts directly to the metadata framework.