Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Clean Boundaries in a Secured ASP.NET Core Application

You are the sole backend engineer on a small digital-library team. The team has agreed on Clean Architecture and scaffolded every layer, but none of them work yet. Over the course of this lab you will grow the repository layer by layer: a domain core that knows nothing about databases, an application layer full of MediatR handlers and FluentValidation validators, an EF Core infrastructure layer, a thin ASP.NET Core Web API, and finally a paged Blazor WebAssembly front-end that sends signed-out users to sign in. Each layer must compile and behave correctly before the next one is added, enforcing the inward-pointing dependency rule at every turn.

Lab platform
Lab Info
Level
Advanced
Last updated
Oct 30, 2026
Duration
1h 15m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Explore the solution structure and architecture goals

    Welcome to the Book Catalog lab

    You are the sole backend engineer on a small digital-library team. The team has agreed on Clean Architecture and scaffolded every layer, but none of them work yet. Over the next five steps you will complete them layer by layer:

    • A Domain layer that knows nothing about databases
    • An Application layer full of MediatR handlers and FluentValidation validators
    • An Infrastructure layer that owns EF Core
    • A thin ASP.NET Core Web API
    • A Blazor WebAssembly front-end that redirects signed-out users to sign in

    Each layer must behave correctly before you build on it, and dependencies must keep pointing inward at every turn. ### Solution layout

    Every layer is a folder in a single project, src/Lab.csproj, with its own namespace (BookCatalog.Domain, BookCatalog.Application, and so on):

    workspace/
    ├── src/
    │   ├── Domain/           ← knows nothing outside itself
    │   ├── Application/      ← depends on Domain only
    │   ├── Infrastructure/   ← depends on Domain; the only place EF Core appears
    │   ├── Api/              ← depends on Application + Infrastructure
    │   ├── BlazorClient/     ← calls the Api through a typed BooksClient
    │   ├── Program.cs        ← a console tour of your work, run in Step 6
    │   └── Lab.csproj        ← the one project that compiles every layer
    └── tests/                ← the checks that grade each task
    

    Dependencies always point inward. Because every layer is in one project, the compiler won't stop an outward reference, so keeping the rule is up to you. Confirm that EF Core stays out of the inner layers:

    grep -rl "EntityFrameworkCore" src/Domain src/Application
    

    It prints nothing. Run it again when you finish Step 4, and it should still print nothing. ### Key types already in place

    The starter scaffold provides:

    • Result<T>: a typed result wrapper with .IsSuccess, .Value, and .Error properties
    • PagedResult<T>: carries Items, TotalCount, Page, and PageSize
    • BookDto record: the projection surface used by the query handler
    • BookCatalogDbContext: EF Core context with a Books DbSet<Book>
    • BooksClient: the typed HTTP client the Blazor book list calls
    • Skeleton files for every class you will complete, each marked with a // TODO: Task N.N comment

    Run the checks now to see your starting line:

    dotnet test tests/Lab.Tests.csproj --nologo --verbosity quiet
    

    All 12 task checks fail: the output lists each failing test, such as Lab.Tests.Step2Tests.Task_2_1, with the reason it failed. Each check starts passing as you complete its task.

  2. Challenge

    Step 2: Build the persistence-ignorant domain layer

    The domain layer: no persistence allowed

    Clean Architecture's central rule is that the domain sits at the center of the dependency graph. It must not reference EF Core, ASP.NET Core, or any other infrastructure concern. Every other layer depends on it; it depends on nothing outside itself.

    A value object is an immutable type whose identity is its value rather than a database key. Isbn is a textbook example: two Isbn instances that hold "9780132350884" are interchangeable. Because the value object owns its own validation logic, you can never construct an Isbn with a malformed value—the factory method makes an invalid state unrepresentable. ### Aggregate roots and factory methods

    An aggregate root is the entry point to a cluster of related objects. External code may only reach the cluster through the root—never by reaching inside and mutating child objects directly. Book is the aggregate root for the book catalog.

    Factory methods on aggregate roots serve two purposes: they enforce invariants (a book must have a valid ISBN and a non-empty title) and they keep construction logic in one place. Using a static Create method that returns Result<Book> means callers handle errors through return values rather than exceptions. ### Repository interfaces belong in the domain

    The domain defines what it needs from persistence without saying how persistence works. The IBookRepository interface lives in Domain/Interfaces/, making the dependency on a storage abstraction explicit. EF Core, in-memory fakes, and SQL all satisfy this interface equally—the domain never knows which one is running.

  3. Challenge

    Step 3: Route CQRS commands and queries through MediatR

    The application layer: commands, queries, and pipelines

    The application layer orchestrates the domain. It contains no business rules of its own; it delegates those to domain entities and value objects. Its job is to accept an intent (a command or a query), validate it, run the correct domain behavior, and return a result.

    CQRS (Command Query Responsibility Segregation) splits reads and writes into separate paths. MediatR is the in-process bus: you Send a request object and a matching IRequestHandler executes it. This decouples controllers from handlers completely—the controller doesn't need to know which handler exists. ### Projecting domain entities to response models

    A domain entity should never escape the application boundary directly. Exposing Book from an API endpoint would couple external clients to your internal model—any refactor breaks the contract. Instead, the query handler projects each Book to a BookDto record before returning it. The PagedResult<BookDto> wrapper adds the paging metadata the Blazor client needs to render navigation controls. ### Pipeline behaviors: cross-cutting concerns without cluttering handlers

    MediatR's IPipelineBehavior<TRequest, TResponse> is the application-layer equivalent of ASP.NET Core middleware. You register one or more behaviors, and every request passes through them in order before reaching the handler. ValidationBehavior uses this hook to run all registered IValidator<TRequest> instances and short-circuit the pipeline when any rule fails. The handler never sees an invalid command—it can assume every invariant held by the validator is already satisfied.

  4. Challenge

    Step 4: Connect EF Core infrastructure and expose API controllers

    The infrastructure layer: where EF Core lives

    EF Core is an infrastructure concern. Putting DbContext calls inside domain entities or application handlers would make the domain depend on persistence—exactly what Clean Architecture forbids. The infrastructure layer is where Microsoft.EntityFrameworkCore gets configured. It implements IBookRepository using BookCatalogDbContext and wires everything up through a DI extension method, so the API host calls a single AddInfrastructure() and does not configure EF Core itself. ### Service registration as a first-class concern

    In ASP.NET Core the DI container is the wiring diagram of your application. Keeping all infrastructure registrations inside an AddInfrastructure extension method means Program.cs stays readable and the infrastructure layer controls its own configuration surface. The host only knows that infrastructure exists; it does not know how EF Core is configured. ### Thin controllers: HTTP in, HTTP out

    A controller's role in Clean Architecture is translation: convert an HTTP request to a MediatR request, send it, and convert the result to an HTTP response. No validation logic, no database calls, no business rules. When a controller has a single await _mediator.Send(...) per action and a small if/else to pick the status code, it is doing exactly the right amount of work.

    The [ProducesResponseType] attributes are not decorative—tools such as NSwag read them from the OpenAPI document to generate accurate client method signatures.

  5. Challenge

    Step 5: Harden, test, and build a paged Blazor client

    Hardening: middleware, logging, and tests

    A production service needs three safety nets that live outside the core request path:

    1. Global exception middleware—catches anything that slips past the handler pipeline and returns a well-formed RFC 9457 ProblemDetails response instead of a raw 500 stack trace.
    2. Structured logging—records the HTTP verb, path, and exception as discrete fields, not a concatenated string, so a log-aggregation tool can filter and group them.
    3. Unit tests—verify that the handler correctly propagates domain errors and that the middleware correctly formats unhandled exceptions, without needing a running server or a real database.

    RFC 9457 (application/problem+json, which replaces RFC 7807) is the IETF standard for HTTP API error responses. Returning it from your middleware means every consumer—the Blazor client, a third-party integration, a developer hitting curl—gets a consistent, machine-readable error envelope. ### Testing handlers in isolation

    The command handler has no HTTP, no EF Core, and no framework lifecycle—it is a plain C# class that takes an interface. That makes it ideal for unit testing. A hand-rolled in-memory fake is preferable to a mocking framework here because it proves that the aggregate is actually handed to AddAsync, not just that the method was called with some argument. Fast, dependency-free tests like these run in every CI build without a database connection. ### Blazor WebAssembly: typed clients and authentication guards

    In production, NSwag reads the OpenAPI document your API emits and generates a strongly-typed BooksClient class, so the Blazor app never hard-codes API routes. This lab ships a hand-written BooksClient in src/BlazorClient/Services/BooksClient.cs so the project builds without a running API.

    The Blazor WebAssembly app runs in the browser, so anyone can load its pages. Treat the guard in OnInitializedAsync as a user-experience check: it reads the AuthenticationState and redirects to /login before any API call is made. In production, the API must also reject unauthenticated requests on the server, which this lab does not cover.

  6. Challenge

    Step 6: Watch a book cross every boundary

    Every task is done and graded. Now run the console tour in src/Program.cs, which sends real commands and queries through the layers you built. You'll watch three things: books traveling from a command to a paged result, two layers refusing bad input, and a crash turning into a clean error response. ### Follow two books from command to page

    In the Terminal tab, run:

    dotnet run --project src/Lab.csproj
    

    Find the Task 3.1 / 3.3 section. The first two commands each print Book created with Id=... with a fresh Guid. Each one went through MediatR, your ValidationBehavior, your handler, Book.Create, and the EF Core BookRepository. The handler only ever saw the IBookRepository interface you declared in the domain.

    The Task 3.2 / 4.1 section reads them back: Page=1, PageSize=10, TotalCount=2, Items returned=2. Domain-Driven Design is listed before Patterns of Enterprise Application Architecture because your repository orders by title. Each ISBN prints as a plain string such as 9780321125217, because your query handler projected Isbn.Value into a BookDto instead of returning the entity. ### Watch two layers refuse bad input

    In the same Task 3.1 / 3.3 section, two more commands are refused:

    • Correctly short-circuited: Title is required. Your validator caught the blank title, and your ValidationBehavior from Task 3.3 returned the failure without calling next, so the handler never ran.
    • Rejected by the domain: Invalid ISBN format. The validator only checks that the ISBN isn't empty, so not-an-isbn got past it. Your handler called Book.Create, and the Isbn value object from Task 2.1 refused the format.

    Four create commands went in, and TotalCount is still 2. Neither bad book reached the database.

    Now see the behavior collect every failure, not just the first. In src/Program.cs, find this line:

    var result = await mediator.Send(new CreateBookCommand("", "9780321125217"));
    

    Change it to:

    var result = await mediator.Send(new CreateBookCommand("", ""));
    

    Rerun the tour. The line now reads Correctly short-circuited: Title is required.; ISBN is required. Your behavior ran every rule and joined the failures, so a client can fix both fields in one go. Put the ISBN back when you're done. ### See a crash become a problem-details response

    The Task 5.1 section sends a POST /api/books request through your ExceptionMiddleware, and the next step in the pipeline throws InvalidOperationException: boom. Two things come out:

    • A fail: log entry that reads Unhandled exception for POST /api/books, followed by the exception and its stack trace. Method and Path are logged as structured properties, so a log tool can filter on them instead of parsing text. The stack trace stays in the server's log; compare it with the response body below, which carries only the message.
    • StatusCode=500, ContentType=application/problem+json and the body {"title":"An unexpected error occurred.","status":500,"detail":"boom"}. Every client gets the same RFC 9457 shape instead of a stack trace.

    The Blazor book list and handler tests do not run during the console demonstration. Their functionality is covered by separate automated tests.

    To verify that all implementations still pass their automated checks, run:

    dotnet test tests/Lab.Tests.csproj --nologo --logger "console;verbosity=normal"
    

    You started with an empty catalog. You now have a domain that refuses bad data, a pipeline that stops blank input before it reaches a handler, and an API that fails safely.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight