VSABlazorTutorialSeptember 2026 · 8 min read

Tutorial Vertical Slice Architecture VSA in Blazor — Part 1: Project Setup & First Slice

TL;DR

Part 1 is the beginner-friendly start of the Tutorial VSA in Blazor series. We create a new .NET 10 Blazor Server project, build the VSA feature-folder structure under Features/Utilities/Todo, wire EF Core and MediatR, define the Todo entity, and implement the first read-only slice — the GetTodoList query, handler, endpoint, service, and a MudBlazor MudTable page. By the end you'll have a running Blazor app with one complete vertical slice.

Part 1 of 2 in the VSA in Blazor 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.

This is the first part of a beginner-friendly, step-by-step tutorial series on Vertical Slice Architecture (VSA) in Blazor. If you're used to architecture articles full of boxes and arrows, this series is the opposite: we create a real project, one step at a time, and you can follow along with dotnet on your own machine. Everything you build here comes from patterns used in a real production Blazor CRM application — the same folder structure, the same handler shape, the same component patterns.

By the end of Part 1, you'll have a running Blazor Server application in .NET 10 with one complete, read-only vertical slice: a Todo list that reads from a database through a MediatR handler and renders in a MudBlazor table. That might not sound like much, but the important thing is where each piece lives. Once you see how one slice is organized, every future feature follows the same path.

Step 1: Create the Blazor Server Project

Open a terminal and create a new .NET 10 Blazor project:

dotnet new blazor -n VsaBlazorTodo
cd VsaBlazorTodo
dotnet run

Take a look at the default structure — you'll see Program.cs, the App.razor root component, and folders like Components and Layout. There is no Controllers, no Services, no Repositories. In VSA we keep it that way: instead of organizing by technical layer, we organize by feature.

Step 2: Add the NuGet Packages

Our first slice needs MediatR for the command/query pipeline, EF Core for data access, and MudBlazor for the UI:

dotnet add package MediatR
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package FluentValidation
dotnet add package MudBlazor

You'll use FluentValidation in Part 2 when we add the create and update slices. For now these packages are everything needed to define an entity, query it, and display it.

Step 3: Build the VSA Feature-Folder Structure

The heart of VSA is the feature folder — one folder per feature containing everything that feature needs. Create this structure:

VsaBlazorTodo/
└── Features/
    └── Utilities/
        └── Todo/
            ├── Cqrs/
            │   ├── GetTodoListHandler.cs
            │   ├── CreateTodoHandler.cs
            │   ├── UpdateTodoHandler.cs
            │   └── DeleteTodoByIdHandler.cs
            ├── Components/
            │   ├── TodoPage.razor
            │   └── _TodoDataTable.razor
            ├── TodoService.cs
            └── TodoEndpoint.cs

The Cqrs folder holds commands, queries, and handlers. The Components folder holds Blazor components. TodoService.cs is the HTTP client wrapper, and TodoEndpoint.cs maps the REST routes. This mirrors Blazor CRM, where the feature is named Utilities/Todo because it's a cross-cutting utility module. You don't need separate projects for "domain" or "infrastructure" — the slice is the boundary.

Step 4: Define the Todo Entity

In VSA, the entity lives with the feature. Create Features/Utilities/Todo/Todo.cs:

namespace VsaBlazorTodo.Features.Utilities.Todo;

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; }
}

Note the audit fields — CreatedAt, CreatedBy, UpdatedAt, UpdatedBy. Blazor CRM keeps these on nearly every entity and fills them in with a shared save interceptor rather than in each handler. The fields exist on the model now so query projections can expose them.

Step 5: Wire EF Core and MediatR in Program.cs

Register the DbContext and MediatR in Program.cs. MediatR scans the assembly for handlers, so one registration line covers every future slice:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

// ...after app is built
app.MapTodoEndpoints();

Because MediatR scans the whole assembly, adding a new IRequestHandler<,> later requires zero changes to Program.cs. That's the "open for extension, closed for modification" benefit VSA gets almost for free.

Step 6: The First Slice — GetTodoList Handler

Now the core of the slice: a query record and a handler implementing IRequestHandler<,>. The handler takes only the DbContext as a dependency and projects entities straight into response DTOs:

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);
    }
}

Notice what's not here: no service class, no repository, no mapping layer. The projection happens inline with a LINQ Select, so EF Core translates it directly to SQL. This is the query pattern used in the real GetTodoListHandler from Blazor CRM, simplified for the tutorial.

Step 7: Expose It with an Endpoint

Add a static endpoint class that maps a route group. The real Blazor CRM group is MapGroup("/todo").RequireAuthorization(...JwtBearer); here we use /api/todo and keep the same shape:

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

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

Calling mediator.Send(new GetTodoListQuery()) dispatches to the handler registered in Step 5. For a tutorial you can comment out the RequireAuthorization line until authentication is wired up in a later part — the route-group shape stays the same either way.

Step 8: The Service Layer

Blazor components shouldn't call HttpClient directly. A small service wraps the API calls. Blazor CRM's TodoService extends a BaseService and uses RestSharp; the essential shape is:

public class TodoService
{
    private readonly RestClient _client;

    public TodoService(NavigationManager nav)
    {
        _client = new RestClient(nav.BaseUri);
    }

    public async Task<ApiResponse<List<GetTodoListResponse>>?> GetTodoListAsync()
    {
        var request = new RestRequest("api/todo", Method.Get);
        return await _client.ExecuteAsync<ApiResponse<List<GetTodoListResponse>>>(request);
    }
}

The ApiResponse<T> wrapper is a standard envelope that carries Success, Message, and Data. Every endpoint returns it, which gives the front end a uniform way to handle success and failure — more on that in Part 2.

Step 9: Render the Slice with MudBlazor

Finally, the read-only page. Create TodoPage.razor with a MudBlazor MudTable:

@page "/todo"
@using VsaBlazorTodo.Features.Utilities.Todo
@using VsaBlazorTodo.Features.Utilities.Todo.Cqrs
@using MudBlazor

<MudTable Items="_todos" ReadOnly Bordered Dense>
    <HeaderContent>
        <MudTh>Auto Number</MudTh>
        <MudTh>Name</MudTh>
        <MudTh>Start</MudTh>
        <MudTh>End</MudTh>
        <MudTh>Status</MudTh>
    </HeaderContent>
    <RowTemplate>
        <MudTd>@context.AutoNumber</MudTd>
        <MudTd>@context.Name</MudTd>
        <MudTd>@context.StartTime</MudTd>
        <MudTd>@context.EndTime</MudTd>
        <MudTd>@(context.IsCompleted ? "Completed" : "Pending")</MudTd>
    </RowTemplate>
</MudTable>

@code {
    private List<GetTodoListResponse> _todos = new();

    protected override async Task OnInitializedAsync()
    {
        var response = await TodoService.GetTodoListAsync();
        if (response?.Success == true) _todos = response.Data ?? new();
    }
}

That's the whole first slice: query record, handler, endpoint, service, and component. In Blazor CRM this page is a state machine that switches between table, create, update, and view modes. Part 2 turns our read-only page into that complete CRUD experience.

Why Feature Folders Beat Layer Folders for Beginners

The main reason this structure is great for learning is traceability. When a bug is in "the todo list," you open one folder and see everything involved — the SQL projection, the route, the HTTP call, and the markup. Nothing lives two folders away. When you add a feature in Part 2, you'll extend this same skeleton, which is exactly how real VSA codebases grow.

Key Takeaways

  • VSA organizes code by feature — one folder per slice containing Cqrs, Components, service, and endpoint
  • MediatR scans the assembly once, so new handlers need zero registration changes
  • The GetTodoListHandler projects entities to DTOs inline with LINQ — no repository or mapper layer
  • A slice is the boundary: query record, handler, endpoint, service, and MudTable page all live in Features/Utilities/Todo/
  • ApiResponse<T> is the uniform envelope every endpoint returns

Frequently Asked Questions

Q: What is VSA in Blazor?

Vertical Slice Architecture in Blazor means organizing the application by feature — a "slice" that cuts through every technical layer — instead of organizing by layer (Controllers, Services, Repositories). Each feature like Todo keeps its CQRS handlers, endpoint, service, and components in one folder, so changes to a feature stay local to that folder.

Q: Do I need MediatR for VSA in Blazor?

No. MediatR is a convenience, not a requirement. It gives you a uniform Send pipeline, automatic handler discovery, and easy middleware (validation, logging) per request. The MVC tutorials in this series build handlers without MediatR by calling HandleAsync directly. Both are valid VSA — pick MediatR when you want the pipeline, plain classes when you want the fewest moving parts.

Q: How many projects does a Blazor VSA solution need?

One. VSA works great in a single project because folders are the organizing unit, not assemblies. Larger teams sometimes split into Application and Infrastructure projects, but the feature folders must stay intact across them. For a beginner tutorial, a single project keeps everything discoverable — open Features/Utilities/Todo and you see the whole feature.

Q: Where do Blazor components live in VSA?

Inside the feature folder, in a Components subfolder. In Blazor CRM, TodoPage.razor and its _TodoDataTable.razor, _TodoCreateForm.razor, and _TodoUpdateForm.razor components all live in Features/Utilities/Todo/Components/. Only truly shared components (like layout shells) move out of the feature folders.

Part 1 of 2 in the VSA in Blazor 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 on GitHub