# The REPR Pattern in ASP.NET Core

> The REPR pattern (Request-Endpoint-Response) replaces bloated controllers with focused, single-purpose endpoints. Here is how to implement it in ASP.NET Core with Minimal APIs.

Published: 2026-07-04. Last updated: 2026-07-14. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/repr-pattern-aspnetcore

The **REPR pattern** (Request-Endpoint-Response) gives each API operation its own class: a strongly typed request, a single-purpose endpoint function, and a strongly typed response.
It replaces bloated controllers with focused endpoint files, and Minimal APIs make it lightweight to implement in ASP.NET Core.

Every controller starts small.
Two actions, one dependency, easy to review.
A year later it has 20 actions, 15 constructor parameters, and a merge conflict every sprint.

## What Is the REPR Pattern?

**REPR** stands for **Request-Endpoint-Response**. It's a pattern for organizing web API code where each endpoint is:

1. **Request** - a strongly-typed object representing the input
2. **Endpoint** - a single function handling one HTTP operation
3. **Response** - a strongly-typed object representing the output

Instead of grouping dozens of actions into controller classes (which violates the Single Responsibility Principle), each endpoint is its own thing.

![Request-Endpoint-Response flow: an HTTP request maps to a typed request object, a single endpoint function, a command or query handler, then a typed response object and the HTTP response](https://milanjovanovic.tech/blogs/articles/repr-pattern-aspnetcore/repr-flow.png)

This is how [**Minimal APIs**](https://milanjovanovic.tech/blog/minimal-apis-dotnet) naturally work - and it pairs perfectly with [**CQRS**](https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start) and [**Clean Architecture**](https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design). The pattern itself was popularized by the FastEndpoints library and Ardalis (Steve Smith), but you don't need any library to use it.

## The Problem With Controllers

Traditional controllers tend to grow into God classes:

```csharp
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;

    // 15 constructor parameters...
    // 20 action methods...
    // 500+ lines of code...

    [HttpPost]
    public async Task<IActionResult> PlaceOrder(PlaceOrderRequest request) { ... }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetOrder(Guid id) { ... }

    [HttpPut("{id}/cancel")]
    public async Task<IActionResult> CancelOrder(Guid id) { ... }

    [HttpGet]
    public async Task<IActionResult> GetOrders([FromQuery] GetOrdersRequest request) { ... }

    // ... 16 more methods
}
```

Problems:
- **Violates SRP** - one class handles many operations with different dependencies
- **Constructor bloat** - every action's dependencies are injected, even if only one action needs them
- **Hard to navigate** - finding the right action in a 500-line file is painful
- **Merge conflicts** - multiple developers editing the same controller file

## REPR With Minimal APIs

Each endpoint becomes a focused, single-purpose function:

### The Request

```csharp
public sealed record PlaceOrderRequest(
    Guid CustomerId,
    List<OrderItemRequest> Items);

public sealed record OrderItemRequest(
    Guid ProductId,
    int Quantity);
```

### The Endpoint

```csharp
public sealed class PlaceOrderEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPost("/api/orders", Handle)
            .WithTags("Orders")
            .WithName("PlaceOrder")
            .Produces<PlaceOrderResponse>(StatusCodes.Status201Created)
            .ProducesProblem(StatusCodes.Status400BadRequest);
    }

    private static async Task<IResult> Handle(
        PlaceOrderRequest request,
        ICommandHandler<PlaceOrderCommand, Guid> handler,
        CancellationToken cancellationToken)
    {
        var command = new PlaceOrderCommand(request.CustomerId, request.Items);

        var result = await handler.Handle(command, cancellationToken);

        return result.IsSuccess
            ? Results.Created($"/api/orders/{result.Value}", new PlaceOrderResponse(result.Value))
            : result.ToProblemDetails();
    }
}
```

`IEndpoint` is a one-method interface that powers auto-discovery.
I define it in the [auto-discovery section](#auto-discovering-endpoints) below.

### The Response

```csharp
public sealed record PlaceOrderResponse(Guid OrderId);
```

Each endpoint lives in its own file with its own request, response, and handler reference. No shared state, no bloated constructor.

## Organizing REPR Endpoints by Feature

```
Api/
  Endpoints/
    Orders/
      PlaceOrderEndpoint.cs
      PlaceOrderRequest.cs
      PlaceOrderResponse.cs
      GetOrderEndpoint.cs
      GetOrderResponse.cs
      CancelOrderEndpoint.cs
    Customers/
      RegisterCustomerEndpoint.cs
      RegisterCustomerRequest.cs
      GetCustomerEndpoint.cs
    Products/
      CreateProductEndpoint.cs
      GetProductsEndpoint.cs
```

Or colocate request/response inside the endpoint class for smaller endpoints:

```csharp
public sealed class CancelOrderEndpoint : IEndpoint
{
    public sealed record CancelOrderResponse(Guid OrderId, string Status);

    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPut("/api/orders/{orderId:guid}/cancel", Handle)
            .WithTags("Orders");
    }

    private static async Task<IResult> Handle(
        Guid orderId,
        ICommandHandler<CancelOrderCommand> handler,
        CancellationToken cancellationToken)
    {
        var command = new CancelOrderCommand(orderId);

        var result = await handler.Handle(command, cancellationToken);

        return result.IsSuccess
            ? Results.Ok(new CancelOrderResponse(orderId, "Cancelled"))
            : result.ToProblemDetails();
    }
}
```

## Auto-Discovering Endpoints

Instead of manually registering each endpoint, define a small interface and scan the assembly at startup:

```csharp
public interface IEndpoint
{
    void MapEndpoint(IEndpointRouteBuilder app);
}
```

```csharp
public static class EndpointExtensions
{
    public static void MapEndpoints(this IEndpointRouteBuilder app)
    {
        var endpointTypes = typeof(Program).Assembly
            .GetTypes()
            .Where(t => t is { IsClass: true, IsAbstract: false } &&
                        t.IsAssignableTo(typeof(IEndpoint)));

        foreach (var type in endpointTypes)
        {
            var endpoint = (IEndpoint)Activator.CreateInstance(type)!;

            endpoint.MapEndpoint(app);
        }
    }
}
```

```csharp
// Program.cs
app.MapEndpoints();
```

Now every class implementing `IEndpoint` is automatically registered.
`Activator.CreateInstance` works here because endpoint classes are stateless (their dependencies arrive as `Handle` parameters).
I walk through a production-ready version of this approach (including DI registration of endpoint classes) in [**automatically registering Minimal APIs**](https://milanjovanovic.tech/blog/automatically-register-minimal-apis-in-aspnetcore), and [**how I structure Minimal APIs**](https://milanjovanovic.tech/blog/how-to-structure-minimal-apis) covers where these files should live.

## REPR + CQRS

REPR and CQRS are natural partners:

- **POST/PUT/DELETE** endpoints map to **Commands**
- **GET** endpoints map to **Queries**

```csharp
// Write endpoint → Command
public sealed class PlaceOrderEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app) =>
        app.MapPost("/api/orders", Handle);

    private static async Task<IResult> Handle(
        PlaceOrderRequest request,
        ICommandHandler<PlaceOrderCommand, Guid> handler,
        CancellationToken ct)
    {
        var command = new PlaceOrderCommand(request.CustomerId, request.Items);
        var result = await handler.Handle(command, ct);
        return result.IsSuccess
            ? Results.Created($"/api/orders/{result.Value}", result.Value)
            : result.ToProblemDetails();
    }
}

// Read endpoint → Query
public sealed class GetOrderEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app) =>
        app.MapGet("/api/orders/{orderId:guid}", Handle);

    private static async Task<IResult> Handle(
        Guid orderId,
        IQueryHandler<GetOrderByIdQuery, OrderResponse> handler,
        CancellationToken ct)
    {
        var result = await handler.Handle(new GetOrderByIdQuery(orderId), ct);
        return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
    }
}
```

Each endpoint is a thin adapter between HTTP and your [**Application layer**](https://milanjovanovic.tech/blog/application-layer-clean-architecture).

## REPR vs MVC Controllers

How the two approaches compare in practice:

- **Granularity**: REPR gives you one class per operation; controllers give you one class per resource with many actions.
- **Dependencies**: a REPR endpoint declares only what that operation needs. A controller's constructor accumulates every action's dependencies.
- **File size**: REPR endpoints stay at 20-50 lines. Controllers routinely grow into hundreds.
- **Merge conflicts**: rare with one file per operation, common when a team shares one controller file.
- **Navigation**: with REPR, the file name is the operation. With controllers, you scroll.
- **Testing**: identical. In both styles the real logic lives in the handler, and the HTTP layer stays thin.
- **OpenAPI metadata**: REPR endpoints declare `Produces` metadata explicitly; `[ApiController]` conventions infer more automatically.

The one scenario where controllers still earn their keep: if you depend heavily on MVC-specific features (filters with complex ordering, model binding conventions, view results), migrating to REPR is a bigger lift than the benefit justifies.

## Do You Need a Library?

Three ways to get REPR in practice:

- **Plain Minimal APIs** (what this article shows): zero dependencies, full control, you own the ~30 lines of auto-discovery code.
- **FastEndpoints**: a mature library built entirely around REPR, with base classes per endpoint, built-in validation, and its own request pipeline. Great if you want conventions decided for you; the cost is a framework layer between you and ASP.NET Core.
- **Single-action controllers**: one controller class per operation. Works if your team must stay on MVC, but you keep the controller ceremony without gaining much.

I default to plain Minimal APIs. The pattern is simple enough that a library is optional, and staying on the framework's primitives means every ASP.NET Core feature (filters, rate limiting, OpenAPI) works without adapter layers.

## Summary

The REPR pattern treats each API operation as an independent unit: one request, one endpoint function, one response.

Combined with Minimal APIs and CQRS, it gives you focused, maintainable endpoint files that map cleanly to your Application layer commands and queries.

Stop writing 500-line controllers. One endpoint, one file, one responsibility.

---

## Frequently asked questions

### What does REPR stand for?

Request-Endpoint-Response. Each API operation gets a strongly typed request object, a single-purpose endpoint function, and a strongly typed response object, instead of living as one action among twenty in a controller.

### Is the REPR pattern the same as Minimal APIs?

No, but they fit naturally. REPR is an organizational pattern (one endpoint, one file, one responsibility). Minimal APIs are the ASP.NET Core feature that makes implementing it lightweight. You can also follow REPR with the FastEndpoints library or even single-action controllers.

### What is wrong with regular MVC controllers?

Nothing inherently, but controllers accumulate actions and dependencies over time. A 500-line controller with 15 constructor parameters violates single responsibility, creates merge conflicts, and makes navigation painful. REPR keeps each operation isolated.

### How do you register REPR endpoints automatically?

Define an IEndpoint interface with a MapEndpoint method, scan the assembly for implementing classes at startup, and call MapEndpoint on an instance of each. This removes the need to register every endpoint manually in Program.cs.
