
Read this article on Medium.
Building full-stack applications with Blazor + .NET 10 can be challenging, especially when aiming for maintainability, testability, and scalability. Vertical Slice Architecture offers a clean, feature-focused approach, organizing code by functionality rather than layers. By encapsulating requests, handlers, endpoints, and UI components within self-contained slices, and sharing DTOs across SSR and WASM, developers can deliver robust features faster while keeping the full-stack predictable and aligned.
In this guide, you’ll learn exactly how to implement Vertical Slice Architecture with Blazor and .NET 10:
- Solution structure
- DTO sharing between server and client
- Commands/queries and handlers
- Validation
- Endpoint mapping
- EF Core integration
- Blazor SSR + Auto mode integration
- Testing slices independently
- Deployment considerations
You’ll also see real code for each section — not theory, but a working, modern, full-stack example.
By the end, you’ll have the blueprint for a scalable, maintainable, future-ready .NET 10 application built on slices.
Why Vertical Slices?
A vertical slice is a feature-focused module that contains everything required for that feature:
Features/
CreateUser/
CreateUserRequest.cs
CreateUserHandler.cs
CreateUserEndpoint.cs
CreateUserPage.razor
UserDto.cs
UserMapping.cs
CreateUserTests.cs
Instead of forcing developers to jump across “Controller → Service → Repository → ViewModel,” everything lives in one cohesive folder.
Slices align perfectly with modern .NET practices: minimal APIs, DTO contracts, MediatR CQRS, Blazor components, and source-generated mapping/validation. They eliminate unnecessary abstractions while improving clarity and testability.
Benefits
Vertical slices reduce development time, increase quality, and make features easier to understand. Each slice is self-contained, maintainable, and testable — improving delivery speed for product and engineering teams.
.NET 10 + Blazor = Perfect Match
.NET 10 introduces improvements that directly support slice-based designs:
- Shared DTOs across Server, SSR, and WASM
- Faster source-generated JSON serialization
- Unified validation using source generators
- Native AOT for APIs
- Blazor Auto Mode, bridging SSR and WASM
- Minimal APIs with per-feature endpoint files
- C# 13 primary constructors for concise handlers
These enhancements make it easier than ever to build end-to-end features that include:
- A Blazor page
- Its corresponding API endpoint
- The command/query request
- Its handler
- Validation
- Mapping
- Unit tests
- Database interaction via EF Core
All within the same slice.
A Quick Glimpse of a Slice in .NET 10
Here’s the smallest possible example of a vertical slice endpoint using .NET 10 minimal APIs and C# 13 syntax:
Request
A request in the MediatR pattern is a simple message object that represents an intention or operation to perform, which is sent to a handler that processes it and returns a result.
public readonly record struct GetUserRequest(Guid Id);
Handler
A handler in the MediatR pattern is the component responsible for receiving a request and executing the logic required to fulfill it, returning the appropriate result.
public class GetUserHandler(AppDbContext db)
{
public async Task<UserDto?> Handle(GetUserRequest request)
=> await db.Users
.Where(u => u.Id == request.Id)
.Select(u => new UserDto(u.Id, u.Email, u.Name))
.FirstOrDefaultAsync();
}
Endpoint
An endpoint is the API entry point that receives an HTTP call, translates it into a MediatR request, and returns the handler’s result as an HTTP response.
public static class GetUserEndpoint
{
public static void Map(IEndpointRouteBuilder app)
{
app.MapGet("/api/users/{id}", async (Guid id, GetUserHandler handler) =>
{
var result = await handler.Handle(new GetUserRequest(id));
return result is null ? Results.NotFound() : Results.Ok(result);
});
}
}
Blazor UI Page
This Blazor page is the UI slice for the feature: it receives the route parameter, calls the API to load the user, and renders the user’s details once the data is retrieved.
@page "/users/{id:guid}"
@inject HttpClient Http
@if (user is null)
{
<p>Loading…</p>
}
else
{
<h3>@user.Name</h3>
<p>@user.Email</p>
}
@code {
[Parameter] public Guid Id { get; set; }
private UserDto? user;
protected override async Task OnInitializedAsync()
{
user = await Http.GetFromJsonAsync<UserDto>($"/api/users/{Id}");
}
}
This entire behavior is self-contained and logically grouped by feature.
What Is Vertical Slice Architecture?
Vertical Slice Architecture is an approach where an application is organized by features rather than by technical layers.
Instead of separating controllers, services, repositories, and UI into different folders, all code related to a single feature is grouped together into one slice.
A vertical slice contains everything required to implement a specific behavior:
Features/
CreateUser/
CreateUserRequest.cs
CreateUserHandler.cs
CreateUserEndpoint.cs
CreateUserPage.razor
UserDto.cs
UserMapping.cs
CreateUserTests.cs
This structure mirrors how teams work and how users perceive the application — by feature, not by underlying technical layer.
Benefits
Vertical slices make the system more modular, easier to change, and simpler to understand. Each feature stands on its own, reducing bottlenecks and allowing developers to ship updates faster with less risk.
Slices encapsulate behavior end-to-end (UI → API → handler → database), improving separation of concerns, lowering coupling, and enabling isolated testing. They replace the traditional controller/service/repository stack with a more focused CQRS-style implementation.
Horizontal Layers vs. Vertical Slices
Horizontal Layers
Traditional .NET applications often look like this:
Controllers/
Services/
Repositories/
Models/
Views/
The behavior for one feature is spread across all those folders.
Vertical Slices
Vertical slices flip that model:
Features/
FeatureA/
FeatureB/
FeatureC/
Each feature has:
- A request (command/query)
- A handler
- Validation
- Mappings
- API endpoint
- Blazor UI page
- Unit Tests
- Supporting DTOs
All in one folder.
Visual Comparison
Traditional Horizontal Architecture
┌────────────┐ ┌────────────┐ ┌─────────────┐
│ Controller ├──▶ │ Service ├──▶ │ Repository │
└────────────┘ └────────────┘ └─────────────┘
▲ ▲ ▲
│ │ │
└──── Feature logic scattered across layers
Vertical Slice Architecture
┌───────────────────────────────┐
│ Feature: CreateUser │
│ ├─ Request │
│ ├─ Handler │
│ ├─ Endpoint │
│ ├─ DTOs │
│ ├─ Validation │
│ ├─ UI Page (Blazor) │
│ └─ Tests │
└───────────────────────────────┘
Behavior is contained, discoverable, testable, and maintainable.
Why Vertical Slices Works So Well With Blazor + .NET 10
Blazor and Minimal APIs naturally align with slices:
- Blazor components are already feature-focused
- .NET 10 encourages smaller, isolated endpoint files
- CQRS (command/query) fits perfectly into slices
- Shared DTOs keep UI and API in sync
- Validation and JSON serialization can be source-generated per slice
Instead of one giant “Services” folder and a massive Program.cs, you get small, clean, digestible slices.
The Problem With Traditional Architectures
Many legacy .NET applications still use a horizontal, layered architecture:
Controllers/
Services/
Repositories/
Models/
Views/
At first, this seems clean: each layer has a clear responsibility. However, as applications grow, several problems emerge.
- Slower feature delivery: Adding a single feature often requires changes in multiple layers and multiple developers.
- Higher cost of change: Fixing bugs may require touching five or six files, increasing risk and QA time.
- Lower predictability: Teams can’t accurately estimate development effort because of hidden dependencies.
For businesses, this translates into slower time-to-market, higher maintenance cost, and frustrated developers.
The technical drawbacks of traditional architecture include:
Feature logic scattered across layers
- A single feature may span controllers, services, repositories, DTOs, and views.
- Developers must navigate multiple files to understand the behavior.
Bloated service classes
- Services often contain unrelated logic for multiple features.
- This creates hidden coupling and makes refactoring risky.
Tight coupling between layers
- Changes in the database or DTOs ripple through services and controllers.
- A small change often triggers modifications in multiple files.
Testing becomes harder
- Unit tests must mock multiple layers.
- Integration tests are often the only reliable way to verify behavior.
UI drift in Blazor apps
- Components and API endpoints may evolve separately.
- Developers may inadvertently break the contract between client and server.
Code Example: A Traditional Feature Spread Across Layers
Controller (Horizontal Layer)
This controller action handles an HTTP GET request, calls the service to retrieve a user by ID, and returns either the user data or a 404 Not Found response.
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(Guid id)
{
var user = await _userService.GetUserByIdAsync(id);
if(user == null) return NotFound();
return Ok(user);
}
Service (Horizontal Layer)
This service method retrieves a user entity by ID from the repository, throws an exception if not found, and maps the entity to a UserDto to return.
public async Task<UserDto> GetUserByIdAsync(Guid id)
{
var entity = await _userRepository.GetByIdAsync(id);
if(entity == null) throw new Exception("User not found");
return new UserDto(entity.Id, entity.Name, entity.Email);
}
Repository (Horizontal Layer)
This repository method fetches a user entity from the database by its ID using Entity Framework Core.
public async Task<User> GetByIdAsync(Guid id)
{
return await _dbContext.Users.FindAsync(id);
}
A simple “get user” feature touches 3 separate folders/files, and any validation, mapping, or UI changes might touch even more. Adding features like logging, caching, or notifications multiplies this complexity.
Why Traditional Architectures Break Down in Modern Apps
- Cross-cutting concerns (logging, caching, notifications) become scattered.
- Code duplication increases.
- Merge conflicts increase because multiple developers touch the same service files.
- Blazor pages require manual coordination with controllers and services.
For a small project, this may be manageable — but in enterprise-scale apps, it becomes unmaintainable.
Transition to Vertical Slices
Vertical Slice Architecture solves these issues by containing each feature in a single folder. Instead of 3–5 files spread across layers, you have one slice with all logic, UI, and tests together.
This reduces coupling, makes testing easier, and accelerates feature delivery.
How Vertical Slices Solve These Problems
Vertical Slice Architecture directly addresses the challenges of traditional layered architectures by organizing code by feature instead of by technical layer. Each feature lives in a single, cohesive folder containing all the logic, UI, data access, validation, and tests required to implement it.
Benefits
- Faster feature delivery: Developers can work on a slice independently without impacting unrelated features.
- Lower risk: Bugs are contained to a single feature.
- Easier onboarding: New developers can understand a feature by examining one folder.
- Improved maintainability: Refactoring is safer because feature logic is localized.
Approach
- Encapsulation: Each slice contains the request, handler, endpoint, DTOs, validation, mapping, and UI.
- Low coupling: Changes in one feature do not ripple through unrelated layers.
- Isolated testing: Handlers and endpoints can be tested independently.
- Alignment with Blazor + .NET 10: UI components, minimal APIs, MediatR requests/handlers, and EF Core logic all live together.
Vertical Slice Example: GetUser Feature
Here’s the same “get user” feature, reorganized as a vertical slice.
Features/
GetUser/
GetUserRequest.cs
GetUserHandler.cs
GetUserEndpoint.cs
GetUserPage.razor
UserDto.cs
UserMapping.cs
GetUserTests.cs
Request (Query) with Validation
Represents the intention to retrieve a user and includes a simple validation attribute to ensure an ID is provided. This prevents invalid requests from reaching the handler.
using System.ComponentModel.DataAnnotations;
public readonly record struct GetUserRequest(
[Required] Guid Id
);
Handler
Executes the database query and maps the User entity to a UserDto using a dedicated mapping method. Keeps the handler focused on business logic.
public class GetUserHandler
{
private readonly AppDbContext _db;
public GetUserHandler(AppDbContext db)
{
_db = db;
}
public async Task<UserDto?> Handle(GetUserRequest request)
{
var entity = await _db.Users
.Where(u => u.Id == request.Id)
.FirstOrDefaultAsync();
if (entity == null) return null;
// Map entity to DTO
return entity.ToDto();
}
}
Mapping Extension
A lightweight mapping layer within the slice, avoiding giant AutoMapper profiles. Encapsulates mapping logic close to the feature.
public static class UserMapping
{
public static UserDto ToDto(this User user)
=> new(user.Id, user.Name, user.Email);
}
Endpoint
Receives the HTTP GET request, validates the request object using built-in attributes, forwards to the handler, and returns the HTTP response.
public static class GetUserEndpoint
{
public static void Map(IEndpointRouteBuilder app)
{
app.MapGet("/api/users/{id}", async (Guid id, GetUserHandler handler) =>
{
var request = new GetUserRequest(id);
// Validate request
var context = new ValidationContext(request);
Validator.ValidateObject(request, context, validateAllProperties: true);
var result = await handler.Handle(request);
return result is null ? Results.NotFound() : Results.Ok(result);
});
}
}Description: The API entry point that receives HTTP requests, forwards them to the handler, and returns the appropriate HTTP response.
Blazor UI Page
The UI component requests the user via the API, displays the data, and handles potential errors. The page is fully contained in the slice, maintaining alignment with the API and handler.
@page "/users/{id:guid}"
@inject HttpClient Http
@if (user is null)
{
<p>Loading…</p>
}
else
{
<h3>@user.Name</h3>
<p>@user.Email</p>
}
@code {
[Parameter] public Guid Id { get; set; }
private UserDto? user;
protected override async Task OnInitializedAsync()
{
try
{
user = await Http.GetFromJsonAsync<UserDto>($"/api/users/{Id}");
}
catch (HttpRequestException)
{
// Handle API errors gracefully
user = null;
}
}
}
Benefits
- Validation is included at the request level, preventing invalid data from reaching the database.
- Mapping is localized, avoiding external dependencies.
- Handler focuses purely on business logic.
- Endpoint handles validation, request forwarding, and HTTP response formatting.
- Blazor UI is tightly aligned with the API.
Everything for one feature lives together, demonstrating how vertical slices improve maintainability, testability, and clarity.
Recommended .NET 10 + Blazor Solution Structure
A well-organized solution is critical when using vertical slices, especially in a full-stack Blazor + .NET 10 application. The goal is to separate features from infrastructure, keep shared contracts centralized, and support both SSR and WASM.
Benefits
- Clear structure reduces onboarding time for new developers.
- Features can be worked on independently without affecting unrelated areas.
- Improves maintainability and reduces risk of errors when adding or changing functionality.
Approach
- Features contain all logic and UI for a specific behavior.
- Infrastructure contains cross-cutting concerns like EF Core, logging, or email services.
- Shared Contracts allow consistent DTOs between server and client, avoiding drift.
- Tests can focus on individual slices or infrastructure.
- Supports Blazor SSR + WASM with minimal duplication.
Suggested Folder Structure
MyApp.sln
│
├── src/
│ ├── Server/ # ASP.NET Core API + SSR
│ │ ├── Features/ # Vertical slices
│ │ │ ├── Users/
│ │ │ │ ├── GetUser/
│ │ │ │ │ ├── GetUserRequest.cs
│ │ │ │ │ ├── GetUserHandler.cs
│ │ │ │ │ ├── GetUserEndpoint.cs
│ │ │ │ │ ├── UserDto.cs
│ │ │ │ │ ├── UserMapping.cs
│ │ │ │ │ └── GetUserTests.cs
│ │ │ │ └── CreateUser/
│ │ │ │ └── ...
│ │ │ └── Products/
│ │ │ └── ...
│ │ ├── Infrastructure/ # Cross-cutting concerns
│ │ │ ├── Db/
│ │ │ ├── Logging/
│ │ │ └── Email/
│ │ └── Program.cs
│ │
│ ├── Client/ # Blazor WASM or Auto mode
│ │ ├── Pages/
│ │ ├── Shared/
│ │ │ └── Components/
│ │ └── App.razor
│ │
│ └── Shared/ # Shared contracts / DTOs
│ ├── Users/
│ │ └── UserDto.cs
│ └── Products/
│ └── ProductDto.cs
│
└── tests/
├── Server.Tests/
│ └── Features/
│ └── Users/
│ └── GetUserHandlerTests.cs
└── Client.Tests/
└── Pages/
└── Users/
└── GetUserPageTests.cs
Explanation of Key Areas
Features Folder
- Contains all slices of your application.
- Each feature folder has everything it needs: requests, handlers, endpoints, mappings, DTOs, UI pages, and tests.
Infrastructure Folder
- Only cross-cutting logic that is shared by multiple slices.
- Examples: EF Core DbContext, logging, caching, email services, background jobs.
Shared Folder
- Centralized DTOs and contracts used across Server, SSR, and WASM.
- Ensures UI and API remain aligned without duplicating code.
Client Folder
- Blazor UI pages and components.
- Can run in SSR, WASM, or Auto mode.
- References shared DTOs to maintain strong typing and prevent drift.
Tests Folder
- Mirrors the structure of Server and Client features.
- Enables isolated unit tests per slice.
- Integration tests for cross-feature behavior if needed.
Benefits of This Structure
- Scalable: New features simply add a new slice; no changes to unrelated code.
- Maintainable: Changes are contained to a single folder.
- Testable: Slice tests can be written in isolation, improving confidence and CI/CD speed.
- Full-stack alignment: SSR and WASM share DTOs and contracts, preventing API/UI drift.
- Supports .NET 10 improvements: Minimal APIs, source-generated DTOs/validation, native AOT, and Blazor Auto mode.
This structure forms the backbone for building large, maintainable full-stack Blazor + .NET 10 applications with vertical slices. Every feature is self-contained, clearly discoverable, and easy to test.
Building a Real Slice (End-to-End Example)
In this section, we’ll build the GetUser feature from scratch using vertical slice architecture in .NET 10 + Blazor. The feature will include:
- Request (Query) with validation
- Handler with EF Core logic
- Mapping to a DTO
- Minimal API Endpoint
- Blazor UI Page
- Unit Test for the handler
This illustrates end-to-end behavior in a single, self-contained feature slice.
Folder Structure for This Slice
Features/
Users/
GetUser/
GetUserRequest.cs
GetUserHandler.cs
GetUserEndpoint.cs
UserDto.cs
UserMapping.cs
GetUserPage.razor
GetUserHandlerTests.cs
Step 1: Request (Query) with Validation
Represents the intention to fetch a user. The [Required] attribute ensures a valid ID is provided before hitting the handler.
using System.ComponentModel.DataAnnotations;
public readonly record struct GetUserRequest(
[Required] Guid Id
);
Step 2: Handler
Contains the business logic to retrieve the user and map it to a DTO. No dependencies on controllers or services outside the slice.
public class GetUserHandler
{
private readonly AppDbContext _db;
public GetUserHandler(AppDbContext db)
{
_db = db;
}
public async Task<UserDto?> Handle(GetUserRequest request)
{
var entity = await _db.Users
.Where(u => u.Id == request.Id)
.FirstOrDefaultAsync();
if (entity == null) return null;
// Map entity to DTO
return entity.ToDto();
}
}
Step 3: Mapping Extension
Lightweight, per-slice mapping from entity to DTO.
public static class UserMapping
{
public static UserDto ToDto(this User user)
=> new(user.Id, user.Name, user.Email);
}
Step 4: Minimal API Endpoint
Handles HTTP GET requests, validates the request, invokes the handler, and returns the correct HTTP response.
public static class GetUserEndpoint
{
public static void Map(IEndpointRouteBuilder app)
{
app.MapGet("/api/users/{id}", async (Guid id, GetUserHandler handler) =>
{
var request = new GetUserRequest(id);
// Validate
var context = new ValidationContext(request);
Validator.ValidateObject(request, context, validateAllProperties: true);
var result = await handler.Handle(request);
return result is null ? Results.NotFound() : Results.Ok(result);
});
}
}
Step 5: Blazor UI Page
The UI slice for the feature; it requests user data via the API and renders it.
@page "/users/{id:guid}"
@inject HttpClient Http
@if (user is null)
{
<p>Loading…</p>
}
else
{
<h3>@user.Name</h3>
<p>@user.Email</p>
}
@code {
[Parameter] public Guid Id { get; set; }
private UserDto? user;
protected override async Task OnInitializedAsync()
{
try
{
user = await Http.GetFromJsonAsync<UserDto>($"/api/users/{Id}");
}
catch (HttpRequestException)
{
user = null;
}
}
}
Step 6: Unit Test for Handler
Tests the handler in isolation using an in-memory database, ensuring the slice logic works without starting the API or UI.
using Xunit;
using Microsoft.EntityFrameworkCore;
public class GetUserHandlerTests
{
[Fact]
public async Task Handle_ReturnsUserDto_WhenUserExists()
{
// Arrange
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.Options;
await using var context = new AppDbContext(options);
var user = new User { Id = Guid.NewGuid(), Name = "Alice", Email = "[email protected]" };
context.Users.Add(user);
await context.SaveChangesAsync();
var handler = new GetUserHandler(context);
var request = new GetUserRequest(user.Id);
// Act
var result = await handler.Handle(request);
// Assert
Assert.NotNull(result);
Assert.Equal(user.Name, result!.Name);
}
}
Benefits
- Encapsulated: Everything for the feature is in one folder.
- Testable: Handler tested independently.
- UI-Aligned: Blazor page and API DTOs stay in sync.
- Maintainable: Adding logging, caching, or notifications stays within the slice.
- Clean: No giant controllers, services, or shared folders clutter the codebase.
Best Practices for Testing Slices
Test the Handler in Isolation
- Mock or use an in-memory database.
- Avoid dependencies on other features or infrastructure when possible.
Test Validation
- Ensure the request object validation behaves as expected.
- Example: Required fields, ranges, or custom validation attributes.
Test the Endpoint
- Minimal API endpoints can be tested using WebApplicationFactory or similar frameworks.
- Ensure the endpoint returns proper HTTP responses for different scenarios.
Test the UI
- Blazor pages can be tested using bUnit for component rendering and behavior.
- Validate that the page correctly calls the API and displays data.
Use Slice-Specific Test Classes
- Keep tests within the same feature folder, mirroring the slice structure.
- Example:
Features/Users/GetUser/GetUserHandlerTests.cs
Isolate Cross-Cutting Concerns
- Mock logging, caching, or email services.
- Keep tests focused on the slice’s core behavior.
Benefits of Slice-Based Testing
- Speed: Tests run quickly because they don’t touch unrelated services.
- Reliability: Isolated slices reduce false positives caused by external dependencies.
- Scalability: Adding new features does not require rewriting old tests.
- Maintainability: Tests live close to the code they validate, improving discoverability.
Shared Contracts and DTOs Across Server and Client
In full-stack Blazor + .NET 10 applications, it’s crucial that the client (Blazor UI) and server (API) share the same data contracts. Vertical slice architecture naturally supports this by centralizing DTOs in a shared project, preventing drift and duplication while ensuring type safety across the stack.
Benefits
- Consistency: UI and API always speak the same language.
- Reduced bugs: No mismatched property names or types.
- Faster delivery: Developers don’t need to manually sync client and server models.
- Maintainability: Single source of truth for data contracts.
Approach
- Create a Shared project for DTOs and other contracts.
- Both Server and Client projects reference the shared project.
- DTOs can include validation attributes, ensuring that both UI and API respect the same rules.
- Blazor pages can deserialize API responses directly into shared DTOs.
Example Shared DTO
Shared Project: MyApp.Shared/Users/UserDto.cs
using System.ComponentModel.DataAnnotations;
public record UserDto(
Guid Id,
[Required] string Name,
[Required, EmailAddress] string Email
);
This DTO defines the data contract for users. Both the Blazor client and the API use this single definition, preventing drift.
Usage in the Server Handler
This handler retrieves a user from the database by ID and maps it to a shared UserDto, encapsulating the business logic for the GetUser feature.
public class GetUserHandler
{
private readonly AppDbContext _db;
public GetUserHandler(AppDbContext db) => _db = db;
public async Task<UserDto?> Handle(GetUserRequest request)
{
var entity = await _db.Users
.Where(u => u.Id == request.Id)
.FirstOrDefaultAsync();
return entity?.ToDto(); // Uses the same UserDto
}
}
Usage in the Blazor Client
The client receives the response and deserializes it directly into the shared UserDto. No manual mapping or separate client-side model is required.
@inject HttpClient Http
@code {
private UserDto? user;
protected override async Task OnInitializedAsync()
{
user = await Http.GetFromJsonAsync<UserDto>("/api/users/1234");
}
}
Advantages of Shared Contracts
- Single Source of Truth: All slices, endpoints, and UI components reference the same DTO.
- Validation Consistency: Attributes on DTOs (e.g.,
[Required]) enforce rules across client and server. - Compile-Time Safety: Renaming a property in the shared DTO will automatically update all usages, reducing runtime errors.
- Simplified Refactoring: Adding a new field propagates to all slices without duplication.
- Supports SSR + WASM: Both Blazor SSR and WASM clients can use the same DTOs, ensuring alignment.
Best Practices
- Keep DTOs lean: Include only fields required for the slice.
- Use record types for immutability and concise code.
- Include validation attributes directly on the DTOs for both API and UI.
- Reference Shared Project in both server and client projects.
- Avoid UI-specific logic in DTOs; keep them pure data contracts.
Folder Structure Example with Shared DTOs
src/
├── Server/
│ └── Features/
│ └── Users/
│ └── GetUser/
├── Client/
│ └── Pages/
│ └── Users/
├── Shared/
│ └── Users/
│ └── UserDto.cs
By sharing DTOs, your Blazor SSR + WASM application maintains strong type safety, alignment between client and server, and easier maintainability, all while keeping slices clean and focused.
Feature-Folder + Slice Structure Best Practices
Vertical Slice Architecture emphasizes organizing code by feature rather than by layer. This section covers best practices for naming, structuring, and scaling slices in a maintainable way.
Benefits
- Features are self-contained, reducing developer friction and cross-team conflicts.
- Clear folder and naming conventions improve discoverability and accelerate onboarding.
- Encourages consistent coding practices across large teams and projects.
Approach
- Each slice contains all components needed for the feature: request, handler, endpoint, mapping, DTOs, UI pages, and tests.
- This reduces tight coupling, improves testability, and ensures UI/API alignment.
- Helps scale large Blazor + .NET 10 applications without creating monolithic service or controller files.
Best Practices for Slice Structure
One Feature, One Folder
Each feature lives in a single folder.
Example:
Features/
Users/
GetUser/
CreateUser/
- Keep related behavior (request, handler, endpoint, UI) together.
- Avoid splitting slices across multiple folders.
Naming Conventions
- Feature folder: Named after the entity and action (e.g.,
GetUser,CreateUser). - Request object:
[Feature]Request(e.g.,GetUserRequest). - Handler:
[Feature]Handler(e.g.,GetUserHandler). - Endpoint:
[Feature]Endpoint(e.g.,GetUserEndpoint). - UI Page:
[Feature]Page.razor(e.g.,GetUserPage.razor). - DTOs: Named after the entity (e.g.,
UserDto).
This makes slices predictable and discoverable.
Keep Mapping Local
- Use per-slice mapping classes or extension methods (
UserMapping.cs) instead of global AutoMapper profiles. - Reduces unnecessary dependencies and keeps each slice self-contained.
Include Validation in the Slice
- Place validation attributes on the request or DTOs.
- Validate at the endpoint or in the handler as needed.
- Keeps business rules close to the feature.
Tests Live Within the Slice
- Mirror slice structure in the tests folder:
Features/
Users/
GetUser/
GetUserHandlerTests.cs
Shared Contracts Go in a Shared Project
- DTOs used by both client and server should live in
Shared/. - Ensures UI and API alignment, reduces duplication, and enforces type safety.
Keep Infrastructure Separate
- Only cross-cutting concerns (database context, logging, email, caching) should reside outside slices.
- Slices should not reference other slices directly — if communication is needed, use MediatR.
Example Folder Layout with Best Practices
Features/
Users/
GetUser/
GetUserRequest.cs
GetUserHandler.cs
GetUserEndpoint.cs
UserDto.cs
UserMapping.cs
GetUserPage.razor
GetUserHandlerTests.cs
CreateUser/
CreateUserRequest.cs
CreateUserHandler.cs
CreateUserEndpoint.cs
UserDto.cs
UserMapping.cs
CreateUserPage.razor
CreateUserHandlerTests.cs
Shared/
Users/
UserDto.cs
Infrastructure/
Db/
Logging/
Email/
Key Points:
- All feature-related logic stays in the slice.
- Shared DTOs go in the
Sharedproject for cross-stack usage. - Infrastructure contains reusable services for multiple slices.
Benefits of Following These Best Practices
- Scalability: Adding new features is straightforward.
- Maintainability: Changes are contained to a single slice.
- Testability: Each slice can be fully unit-tested.
- Predictability: Developers instantly know where to find any feature’s code.
- Alignment: UI, API, and business logic remain consistent.
SSR + WASM Integration Tips
Blazor supports Server-Side Rendering (SSR), WebAssembly (WASM), and a hybrid Auto mode. When building vertical slices, it’s important to structure your slices so that they work seamlessly in all hosting modes.
Benefits
- Flexibility to choose SSR or WASM based on performance, SEO, and user experience.
- Shared feature slices ensure consistent behavior across hosting models.
- Reduces duplicate logic and errors when switching between SSR and WASM.
Approach
- Feature slices should not assume the hosting model.
- DTOs, handlers, and endpoints remain the same; only UI components might differ slightly.
- Use Shared project for DTOs so both client-side and server-side Blazor reference the same contracts.
- Minimal APIs work for SSR and can be called from WASM via HttpClient.
1. Shared DTOs Across SSR + WASM
- Keep DTOs in
Shared/to avoid duplication. - Both SSR pages and WASM components deserialize the same DTOs:
public record UserDto(Guid Id, string Name, string Email);
Any changes to the DTO immediately propagate to all slices.
2. Slice-Based UI Components
- Blazor pages and components reside within the feature slice, whether SSR or WASM:
Features/
Users/
GetUser/
GetUserPage.razor // Blazor page
UserCard.razor // Optional reusable component
- The same Razor components can be rendered in SSR or WASM without modification.
- Components communicate with the API via HttpClient injected via DI.
3. Handling API Calls
- SSR pages can call handlers directly using dependency injection.
- WASM pages call handlers via HTTP endpoints:
SSR Example:
@inject GetUserHandler Handler
@code {
private UserDto? user;
protected override async Task OnInitializedAsync()
{
user = await Handler.Handle(new GetUserRequest(Id));
}
}
WASM Example:
@inject HttpClient Http
@code {
private UserDto? user;
protected override async Task OnInitializedAsync()
{
user = await Http.GetFromJsonAsync<UserDto>($"/api/users/{Id}");
}
}
Tip: Keep your handler logic the same; only the call mechanism differs.
4. Avoid Hosting-Specific Logic in Slices
- Handlers and DTOs should be independent of SSR or WASM.
- Only UI components may require minor adjustments for rendering or lifecycle events.
- This ensures slices remain reusable across hosting models.
5. Use DI Consistently
- Inject handlers, services, or HttpClient depending on SSR or WASM.
- Keep your slices self-contained, referencing only dependencies needed for that feature.
6. Folder Structure for SSR + WASM Integration
Features/
Users/
GetUser/
GetUserRequest.cs
GetUserHandler.cs
GetUserEndpoint.cs
GetUserPage.razor
UserDto.cs
Shared/
Users/
UserDto.cs
Client/
Program.cs
Server/
Program.cs
Highlights:
GetUserHandler.csworks in SSR via DI.- WASM uses
GetUserEndpoint.cs+ HttpClient. - UI components and pages are in the same slice, shared between SSR and WASM.
Benefits of Following These Tips
- Reusability: One slice works for SSR and WASM.
- Consistency: Same DTOs and handlers prevent drift.
- Scalability: New slices follow the same pattern.
- Maintainability: Clear separation of UI vs. business logic vs. API.
Testing Slices Across SSR and WASM
Testing vertical slices becomes slightly more complex when supporting both SSR and WASM, but with a well-structured slice, most logic can still be tested in isolation. The key is to separate business logic, API endpoints, and UI components, then test each appropriately.
Benefits
- Ensures consistent behavior regardless of hosting mode.
- Reduces regressions and drift between SSR and WASM.
- Helps teams validate full-stack features without spinning up multiple environments.
Approach
- Handlers: Test independently with in-memory databases or mocks.
- Endpoints: Test using minimal APIs with
WebApplicationFactoryor test server. - Blazor UI: Test SSR or WASM pages/components using bUnit or integration tests.
- Shared DTOs: Validate contract integrity across client and server.
1. Testing the Handler (Same for SSR & WASM)
- Handler tests are host-agnostic — they work for both SSR and WASM.
- No UI or HTTP dependencies needed.
[Fact]
public async Task Handle_ReturnsUserDto_WhenUserExists()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("TestDbHandler")
.Options;
await using var context = new AppDbContext(options);
var user = new User { Id = Guid.NewGuid(), Name = "Alice", Email = "[email protected]" };
context.Users.Add(user);
await context.SaveChangesAsync();
var handler = new GetUserHandler(context);
var result = await handler.Handle(new GetUserRequest(user.Id));
Assert.NotNull(result);
Assert.Equal("Alice", result!.Name);
}
2. Testing Minimal API Endpoints
Use WebApplicationFactory to host the API in-memory.
- Minimal APIs are tested independently of UI.
- SSR and WASM clients will call the same endpoints, ensuring consistent behavior.
public class GetUserEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public GetUserEndpointTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetUser_ReturnsNotFound_WhenUserDoesNotExist()
{
var response = await _client.GetAsync("/api/users/00000000-0000-0000-0000-000000000000");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
3. Testing Blazor UI Pages with bUnit
bUnit allows rendering Blazor components without a full browser.
- You can mock HttpClient to simulate API responses.
- Works for both SSR and WASM pages.
using Bunit;
[Fact]
public void GetUserPage_DisplaysUser()
{
using var ctx = new TestContext();
ctx.Services.AddSingleton<HttpClient>(new HttpClient(new MockHttpMessageHandler()));
var cut = ctx.RenderComponent<GetUserPage>(parameters => parameters.Add(p => p.Id, Guid.NewGuid()));
cut.MarkupMatches("<p>Loading…</p>");
}
4. Best Practices for Testing Across Hosting Modes
- Keep Handlers Pure: Handlers should contain only business logic, making them reusable in SSR and WASM.
- Test Endpoints Independently: Minimal APIs or controllers are tested via in-memory test servers.
- Test UI Components Separately: Use bUnit or integration tests for Blazor pages. Mock HTTP calls to decouple from backend.
- Use Shared DTOs for Validation: Any change in DTOs triggers test failures if UI/API are out of sync.
Slice-Specific Tests: Keep tests within the same feature folder to mirror the slice structure.
Benefits
- Reliability: SSR and WASM clients behave consistently.
- Isolation: Slice tests are independent of hosting mode.
- Confidence: Teams can deploy features knowing both UI and API are covered.
- Maintainability: Adding new slices follows the same testing strategy.
Maintaining Maintainability: Scaling Vertical Slices
As your Blazor + .NET 10 application grows, vertical slices help, but large apps require disciplined maintainability practices to avoid slices becoming unwieldy or duplicative.
Benefits
- Maintainable slices reduce technical debt.
- Clear boundaries allow multiple teams to work independently.
- Faster onboarding and lower risk of regressions.
- Easier to refactor, extend, and enhance features.
Approach
- Each slice should remain self-contained with minimal external dependencies.
- Use shared DTOs and infrastructure only for cross-cutting concerns.
- Enforce consistent naming, folder structures, and testing.
- Regularly review slices to prevent duplication or unnecessary complexity.
1. Slice Boundaries
- Keep handlers, requests, endpoints, mappings, DTOs, and UI together.
- Avoid referencing other slices directly; use MediatR or events for communication:
// Example: Calling another slice via MediatR
var orders = await _mediator.Send(new GetOrdersByUserRequest(user.Id));
This preserves slice isolation and improves testability.
2. Shared Infrastructure
Place cross-cutting concerns (logging, email, caching, DbContext) in Infrastructure/.
Example:
Infrastructure/
Db/
Logging/
Email/
Caching/
Slices reference these services via DI, not directly.
3. Shared DTOs & Contracts
Keep Shared/DTOs central to avoid drift between SSR and WASM.
Example:
Shared/
Users/
UserDto.cs
Orders/
OrderDto.cs
Enforces type safety across client and server.
4. Testing Discipline
- Maintain slice-specific tests: handlers, endpoints, UI components.
- Regularly run tests for all hosting modes: SSR, WASM, and hybrid.
Example folder layout:
Features/
Users/
GetUser/
GetUserHandlerTests.cs
GetUserEndpointTests.cs
GetUserPageTests.cs
Keeps testing organized and discoverable.
5. Naming & Folder Conventions
Consistency scales better than cleverness:
| Folder/File | Purpose |
| ------------------------ | ---------------------------- |
| `GetUser/` | Slice folder for the feature |
| `GetUserRequest.cs` | Request/Query DTO |
| `GetUserHandler.cs` | Business logic |
| `GetUserEndpoint.cs` | Minimal API endpoint |
| `GetUserPage.razor` | Blazor UI component |
| `UserDto.cs` | Feature-specific DTO |
| `GetUserHandlerTests.cs` | Slice unit tests |
6. Refactoring & Slice Review
- Regularly audit slices to avoid duplication:
- Consolidate common mapping extensions.
- Move repeated UI components to
Shared/Components/. - Refactor complex handlers into smaller services within the slice.
- Encourage per-slice ownership to maintain quality and consistency.
7. Advantages of Scaling Vertical Slices
- Self-contained: Easy to maintain or remove a feature without breaking others.
- Parallel development: Teams work independently on separate slices.
- Testable: Each slice can be validated independently.
- Extensible: Adding new slices does not require modifying unrelated features.
- Clean architecture: SSR + WASM alignment, shared DTOs, and infrastructure separation ensure long-term maintainability.
Features/
Users/
GetUser/
CreateUser/
UpdateUser/
Orders/
GetOrders/
CreateOrder/
Shared/
Users/
UserDto.cs
Orders/
OrderDto.cs
Infrastructure/
Db/
Logging/
Email/
Caching/
Tests/
Server.Tests/
Features/
Users/
GetUserHandlerTests.cs
CreateUserHandlerTests.cs
Orders/
GetOrdersHandlerTests.cs
Client.Tests/
Features/
Users/
GetUserPageTests.cs
Key Takeaways:
- Scalable, self-contained slices support growth without clutter.
- Shared contracts and infrastructure maintain alignment across SSR and WASM.
- Clear naming and structure allow teams to work in parallel confidently.
Vertical Slice Architecture in Blazor + .NET 10 empowers developers to build self-contained, testable, and maintainable features from UI to database. By organizing code by feature, sharing DTOs across SSR and WASM, and keeping handlers and endpoints isolated, teams can scale applications confidently while reducing duplication, ensuring consistency, and accelerating delivery. Following these practices results in a clean, predictable, and robust full-stack architecture that grows gracefully with your product.