# Vertical Slice Architecture With Carter in .NET

> Carter turns Minimal API endpoints into self-registering modules: each feature defines its routes, and app.MapCarter() wires them up at startup. Combined with Vertical Slice Architecture, every feature owns its endpoint, handler, and validator in one folder. Here is the full setup, from installation to integration tests.

Published: 2026-08-13. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/vertical-slice-architecture-carter-dotnet

Carter organizes ASP.NET Core Minimal API endpoints into self-registering modules: each feature defines its routes in an `ICarterModule` class, and `app.MapCarter()` discovers and maps them at startup.
That convention is a natural fit for Vertical Slice Architecture, where each feature already owns its handler and validator.
Here is how I combine the two, from installation to integration tests.

Minimal APIs are great until `Program.cs` hits 500 lines.

## What Is Carter?

[Carter](https://github.com/CarterCommunity/Carter) is a library that adds convention-based routing to ASP.NET Core [**Minimal APIs**](https://milanjovanovic.tech/blog/minimal-apis-dotnet). It lets you define endpoints in self-contained modules instead of one giant `Program.cs`.

Combined with [**Vertical Slice Architecture**](https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet), Carter gives each feature its own endpoint module, handler, and model - all in one place.

## Setting Up Carter

Install the package:

```bash
dotnet add package Carter
```

Register it:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCarter();

builder.Services.AddMediatR(config =>
{
    config.RegisterServicesFromAssembly(typeof(Program).Assembly);
    config.AddOpenBehavior(typeof(ValidationBehavior<,>));
});

builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

var app = builder.Build();

app.MapCarter();
app.Run();
```

`MapCarter()` automatically discovers all `ICarterModule` implementations and registers their routes.

`ValidationBehavior` is the FluentValidation pipeline behavior that runs each slice's validator before the handler and returns failures as a failed `Result`.
I break it down in [**validation in Vertical Slice Architecture**](https://milanjovanovic.tech/blog/validation-vertical-slice-architecture).

![app.MapCarter scans the assembly for ICarterModule implementations and registers the routes each module owns, one module per feature area](https://milanjovanovic.tech/blogs/articles/vertical-slice-architecture-carter-dotnet/carter-module-discovery.png)

## A Feature Slice With Carter

Here's a complete [**feature slice**](https://milanjovanovic.tech/blog/feature-folders-dotnet) for placing an order:

```csharp
// Features/Orders/PlaceOrder.cs
public static class PlaceOrder
{
    public sealed record Command(
        Guid CustomerId,
        List<OrderItemRequest> Items) : IRequest<Result<Guid>>;

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

    public sealed class Validator : AbstractValidator<Command>
    {
        public Validator()
        {
            RuleFor(x => x.CustomerId).NotEmpty();
            RuleFor(x => x.Items).NotEmpty();
            RuleForEach(x => x.Items).ChildRules(item =>
            {
                item.RuleFor(x => x.ProductId).NotEmpty();
                item.RuleFor(x => x.Quantity).GreaterThan(0);
            });
        }
    }

    public sealed class Handler : IRequestHandler<Command, Result<Guid>>
    {
        private readonly ApplicationDbContext _db;

        public Handler(ApplicationDbContext db) => _db = db;

        public async Task<Result<Guid>> Handle(
            Command request, CancellationToken ct)
        {
            var customer = await _db.Customers
                .FirstOrDefaultAsync(c => c.Id == request.CustomerId, ct);

            if (customer is null)
            {
                return Result.Failure<Guid>(
                    new Error("Customer.NotFound", "Customer not found."));
            }

            var order = new Order
            {
                Id = Guid.NewGuid(),
                CustomerId = request.CustomerId,
                Items = request.Items.Select(i => new OrderItem
                {
                    ProductId = i.ProductId,
                    Quantity = i.Quantity
                }).ToList(),
                CreatedAt = DateTime.UtcNow
            };

            _db.Orders.Add(order);
            await _db.SaveChangesAsync(ct);

            return order.Id;
        }
    }
}
```

Now the Carter module:

```csharp
// Features/Orders/OrdersModule.cs
public class OrdersModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders")
            .WithTags("Orders");

        group.MapPost("", async (
            PlaceOrder.Command command,
            ISender sender,
            CancellationToken ct) =>
        {
            var result = await sender.Send(command, ct);

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

        group.MapGet("{id:guid}", async (
            Guid id,
            ISender sender,
            CancellationToken ct) =>
        {
            var result = await sender.Send(new GetOrder.Query(id), ct);

            return result.IsSuccess
                ? Results.Ok(result.Value)
                : result.ToProblemDetails();
        });

        group.MapGet("", async (
            int page,
            int pageSize,
            ISender sender,
            CancellationToken ct) =>
        {
            var result = await sender.Send(
                new GetOrders.Query(page, pageSize), ct);

            return Results.Ok(result.Value);
        });
    }
}
```

`ToProblemDetails()` is a small extension method that maps a failed `Result` to `Results.Problem` or `Results.ValidationProblem`, so every endpoint returns consistent Problem Details responses.

## Project Structure

```
Features/
  Orders/
    PlaceOrder.cs
    GetOrder.cs
    GetOrders.cs
    CancelOrder.cs
    OrdersModule.cs
  Customers/
    RegisterCustomer.cs
    GetCustomer.cs
    CustomersModule.cs
  Products/
    CreateProduct.cs
    GetProducts.cs
    ProductsModule.cs
```

Each feature folder contains:
- **One file per use case** (command/query + handler + validator)
- **One Carter module** for routing all endpoints in that domain

Everything for orders lives in `Features/Orders/`. No jumping between Controllers, Services, Models, and Repositories folders.

## Route Groups and Common Configuration

Carter modules support route groups with shared configuration:

```csharp
public class OrdersModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders")
            .WithTags("Orders")
            .RequireAuthorization();

        group.MapPost("", HandlePlaceOrder);
        group.MapGet("{id:guid}", HandleGetOrder);
        group.MapDelete("{id:guid}", HandleCancelOrder)
            .RequireAuthorization("Admin");
    }

    private static async Task<IResult> HandlePlaceOrder(
        PlaceOrder.Command command,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(command, ct);
        return result.IsSuccess
            ? Results.Created($"/api/orders/{result.Value}", result.Value)
            : result.ToProblemDetails();
    }

    private static async Task<IResult> HandleGetOrder(
        Guid id,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new GetOrder.Query(id), ct);
        return result.IsSuccess
            ? Results.Ok(result.Value)
            : result.ToProblemDetails();
    }

    private static async Task<IResult> HandleCancelOrder(
        Guid id,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new CancelOrder.Command(id), ct);
        return result.IsSuccess
            ? Results.NoContent()
            : result.ToProblemDetails();
    }
}
```

Extract handler methods to keep `AddRoutes` readable.

## Endpoint Filters

If you'd rather handle [**cross-cutting concerns**](https://milanjovanovic.tech/blog/cross-cutting-concerns-in-vertical-slice-architecture) at the HTTP boundary instead of inside the MediatR pipeline, use endpoint filters.
Here's a validation filter that resolves the slice's FluentValidation validator from DI:

```csharp
public class ValidationFilter<TRequest> : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var request = context.Arguments
            .OfType<TRequest>()
            .FirstOrDefault();

        if (request is null)
        {
            return await next(context);
        }

        var validator = context.HttpContext.RequestServices
            .GetService<IValidator<TRequest>>();

        if (validator is not null)
        {
            var result = await validator.ValidateAsync(request);
            if (!result.IsValid)
            {
                return Results.ValidationProblem(result.ToDictionary());
            }
        }

        return await next(context);
    }
}
```

The filter is generic over the request type, so you apply it per endpoint:

```csharp
group.MapPost("", HandlePlaceOrder)
    .AddEndpointFilter<ValidationFilter<PlaceOrder.Command>>();
```

Pick one place to validate (pipeline behavior or endpoint filter), not both.

## Testing Carter Modules

Because Carter modules are just Minimal API routes, they test like any other endpoint through **WebApplicationFactory**:

```csharp
public class OrdersModuleTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public OrdersModuleTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task PlaceOrder_WithInvalidBody_ReturnsProblemDetails()
    {
        var response = await _client.PostAsJsonAsync("/api/orders", new
        {
            CustomerId = Guid.Empty,
            Items = Array.Empty<object>()
        });

        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }
}
```

`MapCarter()` runs during test host startup, so all modules are discovered exactly as in production. No special test setup for Carter itself.

## Why Carter + VSA Works

What the combination buys you:

- **Auto-discovery**: Carter finds modules automatically, no manual registration in `Program.cs`
- **Feature isolation**: each module encapsulates a bounded set of endpoints
- **Clean Program.cs**: just `app.MapCarter()` instead of dozens of `MapGet`/`MapPost` calls
- **Route grouping**: share authorization, filters, and tags across related endpoints
- **Testability**: each handler is independent and easily unit-tested

## Carter vs. Plain Minimal APIs

Without Carter, endpoints pile up in `Program.cs` or require manual extension methods:

```csharp
// Without Carter - gets messy fast
app.MapPost("/api/orders", HandlePlaceOrder);
app.MapGet("/api/orders/{id}", HandleGetOrder);
app.MapGet("/api/orders", HandleGetOrders);
app.MapPost("/api/customers", HandleRegisterCustomer);
app.MapGet("/api/customers/{id}", HandleGetCustomer);
// ... 50 more lines
```

With Carter, each module owns its routes. `Program.cs` stays clean.

To be fair, Carter isn't the only way to get there. You can build the same auto-discovery yourself with an `IEndpoint` interface and assembly scanning, which I showed in [**automatically registering Minimal APIs**](https://milanjovanovic.tech/blog/automatically-register-minimal-apis-in-aspnetcore). Choose Carter if you want the convention ready-made and don't mind a third-party dependency in your API layer; roll your own if you'd rather own those 30 lines of reflection.

## Summary

Carter doesn't change what a vertical slice is.
It standardizes how a slice exposes its routes: each feature folder gets a module, each module owns its endpoints, and `Program.cs` shrinks to `app.MapCarter()`.

If you want that convention ready-made, use Carter.
If you'd rather avoid the dependency, the `IEndpoint` approach gets you the same result for 30 lines of your own code.
Either way, if you're building with Minimal APIs and [**vertical slices**](https://milanjovanovic.tech/blog/vertical-slice-architecture-is-easier-than-you-think), the endpoints should end up where they belong: inside the slice.

Thanks for reading, and stay awesome!

---

## Frequently asked questions

### What is Carter in .NET?

Carter is an open-source library that organizes ASP.NET Core Minimal API endpoints into modules. Each class implementing ICarterModule defines its own routes, and app.MapCarter() discovers and registers all of them automatically.

### Do I need Carter to use Vertical Slice Architecture?

No. You can achieve the same organization with extension methods per feature or a small IEndpoint abstraction with assembly scanning. Carter just gives you the module convention and auto-discovery out of the box.

### Does Carter work with MediatR?

Yes, and it is a common pairing: the Carter module maps HTTP routes and delegates to MediatR commands and queries via ISender, keeping the endpoint layer thin.

### Is Carter production-ready?

Yes. Carter is a thin convention layer over Minimal APIs, so at runtime your endpoints are plain ASP.NET Core route handlers with the same performance and middleware behavior.
