← Back to Blog

Building Maintainable .NET Applications with Vertical Slice Architecture

12 min read Architecture

Introduction

For years, .NET developers have been organizing applications using traditional layered architecture—Controllers in one folder, Services in another, Data Access elsewhere. While this approach follows separation of concerns, it often leads to scattered code, difficult navigation, and tight coupling across layers. Enter Vertical Slice Architecture, a paradigm shift that organizes code by features rather than technical layers.

In this comprehensive guide, we'll explore how VanillaSlice Framework implements vertical slice architecture to create maintainable, scalable .NET applications that work across Blazor Web and MAUI Hybrid platforms.

The Problem with Traditional Layered Architecture

Let's start with a typical scenario. You're building an e-commerce platform and need to add a new "Order Management" feature. In traditional layered architecture, here's what you'll do:

MyApp/
├── Controllers/
│   ├── UserController.cs
│   ├── ProductController.cs
│   └── OrderController.cs        ← Add here
├── Services/
│   ├── UserService.cs
│   ├── ProductService.cs
│   └── OrderService.cs            ← Add here (with LINQ queries)
├── Data/
│   ├── Entities/
│   │   ├── User.cs
│   │   ├── Product.cs
│   │   └── Order.cs               ← Add here
│   └── AppDbContext.cs            ← Reference here
└── Models/
    ├── UserDto.cs
    ├── ProductDto.cs
    └── OrderDto.cs                 ← Add here

Problems this creates:

  • Scattered Code: Logic for a single feature is spread across 5+ directories
  • Difficult Navigation: Finding all order-related code requires searching multiple folders
  • Merge Conflicts: Multiple developers working on different features touch the same folders
  • High Cognitive Load: Understanding one feature requires jumping between layers
  • Unnecessary Abstraction: Repository pattern adds indirection without clear benefits when using EF Core (which is already an abstraction over SQL)

Introducing Vertical Slice Architecture

Vertical Slice Architecture organizes code by features (use cases) rather than technical layers. Each slice contains everything needed for that feature:

MyApp/
└── Features/
    ├── Products/
    │   ├── ProductListing/
    │   │   ├── ProductListingBusinessModel.cs
    │   │   ├── IProductListingDataService.cs
    │   │   ├── ProductListingServerDataService.cs
    │   │   ├── ProductListingController.cs
    │   │   ├── ProductListingClientDataService.cs
    │   │   └── ProductListing.razor
    │   └── ProductForm/
    │       └── [Similar structure]
    └── Orders/
        ├── OrderListing/
        │   └── [Everything for listing orders]
        └── OrderForm/
            └── [Everything for order forms]

Benefits:

  • Feature Cohesion: All code for "Product Listing" lives together
  • Easy Navigation: No hunting across layers—everything's in one place
  • Reduced Conflicts: Teams work on different slices without stepping on each other
  • Lower Cognitive Load: Understand one feature at a time
  • Clear Boundaries: Each slice is independently testable and deployable

VanillaSlice Implementation: The Four-Layer Pattern

VanillaSlice enforces SOLID principles through a consistent four-layer implementation within each slice:

1. Service Contract (Interface)

// ServiceContracts/Features/Products/IProductListingDataService.cs
namespace MyApp.ServiceContracts.Features.Products
{
    public interface IProductListingDataService
        : IListingDataService<ProductListingBusinessModel, ProductFilterBusinessModel>
    {
        // Framework provides:
        // Task<PagedDataList<ProductListingBusinessModel>> GetPaginatedItemsAsync(
        //     ProductFilterBusinessModel filter);

        // Add custom methods here if needed
    }
}

This interface is the contract that all implementations must follow. It lives in a shared assembly referenced by both client and server.

2. Server Data Service (Business Logic with Direct LINQ)

// Server.DataServices/Features/Products/ProductListingServerDataService.cs
namespace MyApp.Server.DataServices.Features.Products
{
    internal class ProductListingServerDataService : IProductListingDataService
    {
        private readonly AppDbContext context;

        public ProductListingServerDataService(AppDbContext context)
        {
            this.context = context;
        }

        public override IQueryable<ProductListingBusinessModel> GetQuery(
            ProductFilterBusinessModel filter)
        {
            // Direct LINQ queries - no repository abstraction needed!
            // EF Core is already an abstraction over SQL
            return from p in context.Products
                   where (string.IsNullOrEmpty(filter.Name) || p.Name.Contains(filter.Name)) &&
                         (!filter.MinPrice.HasValue || p.Price >= filter.MinPrice.Value))
                   select new ProductListingBusinessModel
                   {
                       Id = p.Id,
                       Name = p.Name,
                       Description = p.Description,
                       Price = p.Price,
                       CreatedAt = p.CreatedAt
                   };
        }
    }
}

This is where your business logic lives—direct EF Core LINQ queries, business rules, validation, etc. No repository pattern overhead—the service directly accesses DbContext, keeping the code simple and readable.

3. API Controller (HTTP Endpoint)

// Server.DataServices/Controllers/Products/ProductListingController.cs
namespace MyApp.Server.DataServices.Controllers.Products
{
    [ApiController, Route("api/[controller]/[action]")]
    public class ProductListingController : ControllerBase, IProductListingDataService
    {
        private readonly IProductListingDataService dataService;

        public ProductListingController(IProductListingDataService dataService)
        {
            this.dataService = dataService;
        }

        [HttpGet]
        public async Task<PagedDataList<ProductListingBusinessModel>>
            GetPaginatedItemsAsync([FromQuery] ProductFilterBusinessModel filter)
        {
            return await dataService.GetPaginatedItemsAsync(filter);
        }
    }
}

The controller is thin—it only handles HTTP concerns. All business logic delegates to the service.

4. Client Data Service (HTTP Client)

// ClientShared/Features/Products/ProductListingClientDataService.cs
namespace MyApp.ClientShared.Features.Products
{
    internal class ProductListingClientDataService : IProductListingDataService
    {
        private readonly BaseHttpClient httpClient;

        public ProductListingClientDataService(BaseHttpClient httpClient)
        {
            this.httpClient = httpClient;
        }

        public async Task<PagedDataList<ProductListingBusinessModel>>
            GetPaginatedItemsAsync(ProductFilterBusinessModel filter)
        {
            var url = "api/productListing/GetPaginatedItems" +
                      filter.ToQueryString();

            return await httpClient.GetFromJsonAsync<
                PagedDataList<ProductListingBusinessModel>>(url);
        }
    }
}

The client service wraps HTTP calls. It implements the same interface as the server service, enabling dependency injection swapping.

Why Direct LINQ? (No Repository Pattern)

VanillaSlice intentionally avoids the repository pattern for several pragmatic reasons:

1. EF Core IS Already a Repository

// EF Core DbContext already provides:
// - Unit of Work pattern (SaveChanges)
// - Query abstraction (LINQ)
// - Change tracking
// - Transaction management

// Adding a repository on top is redundant:
public interface IProductRepository  // ❌ Unnecessary layer
{
    Task<Product> GetByIdAsync(int id);
    Task<List<Product>> GetAllAsync();
}

// When you can just do:
var product = await context.Products  // ✅ Clean & direct
    .FirstOrDefaultAsync(p => p.Id == id);

2. Readable, Composable Queries

With direct LINQ, you write one composable method instead of an explosion of repository methods for every filter combination.

VanillaSlice Philosophy: "Don't add abstraction layers just because they're in the textbook. Add them when they solve a real problem. EF Core + Service layer is enough abstraction for 90% of applications."

Multi-Platform Magic: One Codebase, Multiple Platforms

Here's where VanillaSlice shines. The same interface works across platforms:

Blazor WebAssembly:

builder.Services.AddScoped<IProductListingDataService,
    ProductListingClientDataService>();

MAUI Hybrid (Offline):

if (connectivityService.IsOnline)
    builder.Services.AddScoped<IProductListingDataService,
        ProductListingClientDataService>();
else
    builder.Services.AddScoped<IProductListingDataService,
        ProductListingOfflineDataService>();

The UI component doesn't change! This exact Razor component runs on Blazor Server, Blazor WebAssembly, and MAUI Hybrid across all platforms.

Conclusion

Vertical Slice Architecture isn't just a buzzword—it's a pragmatic approach to building maintainable .NET applications. VanillaSlice Framework makes it accessible through:

  • Enforced Structure: Four-layer pattern in every slice
  • Code Generation: SliceFactory automates creation
  • Multi-Platform: Same code runs on Web and Mobile
  • SOLID Principles: Interface-driven design by default

The result? Faster development, fewer bugs, easier maintenance, and happier developers.

Ready to try it?

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.