VSATutorialSeptember 2026 · 8 min read

VSA Todo App — Part 1: Project Setup & First Feature Slice

TL;DR

Part 1 walks through creating a new .NET 10 project with Vertical Slice Architecture, setting up the folder structure, configuring EF Core and MediatR, defining the Todo entity, and implementing your first working feature slice — List Todos with a Minimal API endpoint. By the end, you'll have a running VSA application with a single working endpoint.

Part 1 of 20 in the VSA Todo App Tutorial Series | Next: Part 2 →

Help Us Grow

Love this tutorial? Explore our ready-to-use enterprise starter kits built with VSA in .NET 10.

Building a complete application with Vertical Slice Architecture (VSA) starts with getting the project structure right. In this first part of the VSA Todo App tutorial series, we'll create a new .NET 10 project, set up the folder conventions that make VSA work, configure the database and MediatR, define our Todo entity, and implement the first feature slice — listing all todos. Everything we build here comes from real production code in the Blazor CRM and MVC Project Manager applications, both of which ship complete VSA Todo features.

The key insight of VSA is that every feature should be self-contained. Instead of scattering code across Controllers, Services, Repositories, and Models folders, VSA puts everything a feature needs — its endpoints, handlers, DTOs, validators, and views — inside a single feature folder. When you need to modify the Todo feature, you open one folder. When you add a new feature, you add one folder. This is the exact pattern used in every Indotalent product.

Creating the Project

Create a new ASP.NET Core Web API project targeting .NET 10. Add the NuGet packages you'll need: MediatR, Microsoft.EntityFrameworkCore.SqlServer, and FluentValidation. The project is a single-project monolith — one .csproj file, one Program.cs. VSA works best with fewer projects because it eliminates cross-project references and keeps feature boundaries inside code organization rather than assembly boundaries.

The VSA Folder Structure

The folder structure is the backbone of VSA. Create a Features/ folder at the project root. Inside it, create Features/Utilities/Todo/. This is your first vertical slice. Every file related to the Todo feature lives here. The structure follows this pattern, taken directly from the Blazor CRM production codebase:

Features/Utilities/Todo/
├── Cqrs/                          # Commands, queries, handlers, validators
│   ├── CreateTodoHandler.cs
│   ├── UpdateTodoHandler.cs
│   ├── GetTodoListHandler.cs
│   ├── GetTodoByIdHandler.cs
│   ├── DeleteTodoByIdHandler.cs
│   ├── CreateTodoValidator.cs
│   └── UpdateTodoValidator.cs
├── Endpoints/
│   └── TodoEndpoint.cs            # Minimal API route definitions
└── Components/                    # Blazor UI components (or Views/ for MVC)
    └── TodoPage.razor

The Cqrs/ folder contains all command and query handlers plus their validators. Each handler file defines its own request DTO, response DTO, and handler class — everything co-located in one file. The Endpoints/ folder maps these handlers to HTTP routes. The Components/ folder (or Views/ for MVC) holds the UI. This is the structure used in production across the Blazor CRM Todo feature.

Defining the Todo Entity

The Todo entity is a standard EF Core entity. Create it at Data/Entities/Todo.cs (shared across features):

public class Todo
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string? AutoNumber { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public DateTime? StartTime { get; set; }
    public DateTime? EndTime { get; set; }
    public bool IsCompleted { get; set; }
    public DateTimeOffset? CreatedAt { get; set; }
    public string? CreatedBy { get; set; }
    public DateTimeOffset? UpdatedAt { get; set; }
    public string? UpdatedBy { get; set; }
    public List<TodoItem> TodoItemList { get; set; } = new();
}

Notice the audit fields — CreatedAt, CreatedBy, UpdatedAt, UpdatedBy. These are automatically populated by the DbContext and are essential for production applications. The TodoItemList navigation property sets up the parent-child relationship we'll explore in Part 6.

Configuring the DbContext

Add a DbSet<Todo> to your AppDbContext and configure it in OnModelCreating. Register the DbContext in Program.cs with your SQL Server connection string. Register MediatR with builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>()). This is all standard .NET setup — VSA doesn't require special infrastructure.

Implementing the First Feature Slice: List Todos

The first slice we build is the simplest: listing all todos. Create Features/Utilities/Todo/Cqrs/GetTodoListHandler.cs. In VSA, the handler file contains the query, the response DTO, and the handler class — all in one place:

// GetTodoListHandler.cs — one file, one feature operation
public class GetTodoListResponse
{
    public string? Id { get; set; }
    public string? AutoNumber { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public DateTime? StartTime { get; set; }
    public DateTime? EndTime { get; set; }
    public bool IsCompleted { get; set; }
    public DateTimeOffset? CreatedAt { get; set; }
    public string? CreatedBy { get; set; }
    public DateTimeOffset? UpdatedAt { get; set; }
    public string? UpdatedBy { get; set; }
}

public record GetTodoListQuery() : IRequest<List<GetTodoListResponse>>;

public class GetTodoListHandler : IRequestHandler<GetTodoListQuery, List<GetTodoListResponse>>
{
    private readonly AppDbContext _context;
    public GetTodoListHandler(AppDbContext context) => _context = context;

    public async Task<List<GetTodoListResponse>> Handle(
        GetTodoListQuery request, CancellationToken cancellationToken)
    {
        return await _context.Todo
            .AsNoTracking()
            .OrderByDescending(x => x.CreatedAt)
            .Select(x => new GetTodoListResponse
            {
                Id = x.Id, AutoNumber = x.AutoNumber,
                Name = x.Name, Description = x.Description,
                StartTime = x.StartTime, EndTime = x.EndTime,
                IsCompleted = x.IsCompleted,
                CreatedAt = x.CreatedAt, CreatedBy = x.CreatedBy,
                UpdatedAt = x.UpdatedAt, UpdatedBy = x.UpdatedBy
            })
            .ToListAsync(cancellationToken);
    }
}

This is the entire List Todos feature in one file. The GetTodoListQuery record is the input, the GetTodoListResponse class is the output DTO, and the handler performs the database query with AsNoTracking() for read-only performance and Select projection to avoid loading unnecessary columns. No repository layer, no service layer — just the query and the data it needs.

Mapping the Endpoint

Create Features/Utilities/Todo/Endpoints/TodoEndpoint.cs to expose the handler as an HTTP endpoint:

public static class TodoEndpoint
{
    public static void MapTodoEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/todo")
            .RequireAuthorization(policy => policy
                .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                .RequireAuthenticatedUser());

        group.MapGet("/", async (IMediator mediator) =>
        {
            var result = await mediator.Send(new GetTodoListQuery());
            return Results.Ok(result);
        });
    }
}

Call app.MapTodoEndpoints() in Program.cs. The endpoint is now live at GET /api/todo. The MapGroup with RequireAuthorization protects all Todo endpoints with JWT authentication in one line — every endpoint under this group inherits the auth requirement automatically.

What We Built and What's Next

In Part 1, we created the project, set up the VSA folder structure, defined the Todo entity, configured EF Core and MediatR, and implemented our first working feature slice — listing todos via a Minimal API endpoint. The project compiles, the endpoint works, and the architecture is clean. In Part 2, we'll add Create, Update, and Delete command handlers and explore the two CQRS patterns used in production: MediatR-based handlers from Blazor CRM and manual handler classes from MVC Project Manager.

Key Takeaways

  • VSA organizes code by feature, not by layer — one folder per feature, all related code co-located
  • The folder structure (Features/FeatureName/Cqrs/, Endpoints/, Components/) is the same pattern used in all Indotalent products
  • Each handler file contains the request, response, and handler logic — maximum cohesion in one file
  • Minimal API MapGroup with RequireAuthorization protects all endpoints in a feature with one line
  • Start with a single project — VSA eliminates the need for multi-project solutions

Frequently Asked Questions

Q: What is Vertical Slice Architecture in simple terms?

VSA organizes code by business feature instead of technical layer. Instead of having separate Controllers, Services, and Repositories folders, everything for one feature (like "Create Todo") lives in one folder. This means you can understand and modify a feature by opening one file, not five.

Q: Do I need MediatR for VSA?

No. MediatR is a convenience that provides a clean request/response pipeline, but VSA works with plain handler classes called directly. We'll compare both approaches in Part 2. The folder structure and co-location principle work regardless of whether you use MediatR.

Q: How many projects should my VSA solution have?

One. VSA thrives in single-project monoliths because it organizes code internally through folder structure rather than assembly boundaries. Every Indotalent product is a single .csproj with 50+ feature slices. Fewer projects mean simpler builds, faster compilation, and no circular dependency headaches.

Q: What's the minimum .NET version for VSA?

VSA works with any .NET version. The folder structure and co-location principles are framework-agnostic. However, .NET 10's Minimal APIs, record types, and top-level statements make VSA especially concise. The code examples in this series target .NET 10.

Q: How is VSA different from Clean Architecture?

Clean Architecture organizes by technical layer (Domain, Application, Infrastructure, Presentation) across multiple projects. VSA organizes by business feature within fewer projects. Clean Architecture is great for large teams with strict separation of concerns; VSA is faster to develop and easier to navigate. Both can use CQRS patterns.

Part 1 of 20 in the VSA Todo App Tutorial Series | Next: Part 2 →

Ready to study real VSA code?

Every Indotalent product is a complete .NET 10 application built with Vertical Slice Architecture. Complete source code — $21 each.

Explore Products

Looking for a ready-to-use traditional monolithic multilayered clean architecture?

Monolithic Clean Architecture — 1,300+ devs, 500+ forks. Clean Arch + CQRS + Repository Pattern. Free & open source for commercial use. ⭐ Star our repo or ❤️ buy our products — your support means everything!

Star on GitHub