Backend Development with .NET
Session 03
First .NET Web API
& Project Architecture
Eng. Seif Mansour  ·  Andalusia Academy
Week 2  ·  2.5 hours
Session Goals

By the end of this session, you will be able to:

  • Create and run a .NET Web API project from scratch using the CLI
  • Navigate and explain every file produced by dotnet new webapi
  • Describe the layered architecture — Controllers, Services, and Repositories — and explain why each layer exists
  • Understand what dependency injection is, how .NET's container works, and the three service lifetimes
  • Write a working TasksController with GET and POST endpoints using in-memory data
Session Agenda
Time Segment Type Duration
0:00Tour of the generated projectDemo20 min
0:20Middleware pipeline explainedTheory15 min
0:35Layered architecture — the whyTheory20 min
0:55Dependency injection in .NETTheory + Demo25 min
1:20Break10 min
1:30Build the first TasksControllerLab40 min
2:10Code review & discussionDiscussion20 min
Tour of the Generated Project
Walk through every file that dotnet new webapi creates and understand what each one does before writing a single line of your own code
In this section
  • The project file structure
  • Program.cs — the entry point
  • appsettings.json and launchSettings.json
  • The sample controller to delete
The Generated File Structure
dotnet new webapi -n TaskManagerApi.Api
TaskManagerApi.Api/
├── Controllers/
│   └── WeatherForecastController.cs   # delete this — sample only
├── Properties/
│   └── launchSettings.json            # controls how the app starts locally
├── appsettings.json                   # configuration values
├── appsettings.Development.json       # dev overrides (gitignored by default)
├── Program.cs                         # entry point — services + pipeline wired here
└── TaskManagerApi.Api.csproj          # project file, NuGet package references
First thing to do
Delete WeatherForecastController.cs and its model. It is a scaffold sample with no relation to your project.
Program.cs — The Entry Point
  • Builder phase — register all services into the DI container before the app starts
  • Pipeline phase — add middleware in the exact order requests should flow through
  • app.MapControllers() — connects incoming URLs to controller action methods
  • app.Run() — starts the HTTP server and blocks until the process exits
Minimal hosting model
.NET 6+ uses a top-level Program.cs with no Startup class. Everything is in one file.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();
Middleware Pipeline
Every incoming HTTP request travels through a chain of middleware components before reaching your controller. Understanding this chain is essential for adding auth, logging, and error handling.
In this section
  • What middleware is
  • The request / response pipeline
  • Short-circuiting the pipeline
  • Order matters
The Middleware Pipeline
Request HTTPS Redirection Authorization may return 401 short-circuit Routing Controller your code runs here Response
  • Middleware is code that runs on every request in a defined order
  • Each component can inspect, modify, or short-circuit the pipeline — returning a response before reaching the next step
  • The order you call app.UseXxx() in Program.cs is the order they execute
  • You will add custom middleware in Session 05 (error handling) and Session 16 (rate limiting, CORS)
Layered Architecture
All projects in this course follow a three-layer structure. Learning it now pays off in every session from Session 09 onwards.
In this section
  • Controllers, Services, Repositories
  • Responsibility of each layer
  • Why separation matters
  • The folder structure to adopt now
Controllers → Services → Repositories
  • Controllers — handle HTTP in and out. Receive a request, call a service, return a response. No business logic lives here.
  • Services — all business logic. No knowledge of HTTP, no knowledge of the database. Pure decision-making.
  • Repositories — data access only. Issue queries, return entities. No business rules live here.
React analogy
This mirrors separating your components (Controllers), hooks (Services), and API calls (Repositories) in a React app.
Controllers HTTP in / out · no business logic Services Business logic · no HTTP knowledge Repositories Data access only · no business rules
Why Separate the Layers?
  • Testability — you can unit-test a TaskService without an HTTP server or a database. This is covered fully in Session 17.
  • Replaceability — swap the database (e.g. SQL Server for PostgreSQL) by rewriting only the Repository layer. The Service layer never changes.
  • Readability — a controller that is 20 lines long and only delegates is easy to understand. A controller with SQL queries buried in it is not.
  • Team scaling — developers can own different layers independently without stepping on each other.
Rule of thumb
If you can answer "what does this class do?" in one sentence using only one of the words receive, decide, or query, the layer is correct.
Adopt This Folder Structure Now
TaskManagerApi.Api/
TaskManagerApi.Api/
├── Controllers/            # HTTP layer — one file per resource
├── Services/
│   └── Interfaces/         # ITaskService.cs lives here
├── Repositories/
│   └── Interfaces/         # ITaskRepository.cs lives here
├── Models/
│   ├── Entities/           # TaskItem.cs — the database model
│   └── DTOs/               # request / response shapes (introduced in Session 11)
└── Program.cs
Create the DTOs folder now
You will not use it until Session 11, but establishing the structure early avoids a larger reorganisation mid-course.
Dependency Injection in .NET
DI is the mechanism that connects your three layers together. .NET has a first-class DI container built in — no third-party library required.
In this section
  • What dependency injection is
  • The three service lifetimes
  • Registering services in Program.cs
  • Constructor injection in a controller
What is Dependency Injection?
  • The problem without DI: a class creates its own dependencies with new TaskService() — it is tightly coupled, impossible to swap, and impossible to test in isolation
  • DI inverts this: objects declare what they need (via constructor parameters) and a container provides it — the class never calls new
  • The container is IServiceCollection — you describe every service once in Program.cs, then the framework resolves them automatically
  • Why interfaces? Depending on ITaskService rather than TaskService means you can substitute a different implementation (e.g. a test double) without touching the controller
Service Lifetimes

How long does the container keep an instance alive?

Your default
Scoped
One instance per HTTP request. Created at the start of a request, disposed at the end. Use this for services and repositories.
Singleton
One instance for the entire application lifetime. Use for stateless utilities — a cache wrapper, a logger adapter. Never inject a Scoped into a Singleton.
Transient
New instance every time the container resolves it. Rarely needed. Use only for very lightweight, stateless operations.
Captive dependency trap
Never inject a shorter-lived service into a longer-lived one (e.g. a Scoped into a Singleton). The container will warn you at startup.
Registering Services in Program.cs
Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<ITaskService, TaskService>();
builder.Services.AddScoped<ITaskRepository, TaskRepository>();

var app = builder.Build();
// ... pipeline config below

The pattern is always: AddScoped<IInterface, ConcreteClass>(). The container maps the interface to the implementation — callers only ever see the interface.

Registration order
Register services before calling builder.Build(). After that the container is sealed and no further registrations are possible.
Constructor Injection
Controllers/TasksController.cs
public class TasksController : ControllerBase
{
    private readonly ITaskService _taskService;

    public TasksController(ITaskService taskService)
    {
        _taskService = taskService;
    }
}
  • Declare the dependency as a private readonly field typed to the interface
  • Accept it as a constructor parameter — the container injects the concrete instance automatically
  • The controller never calls new TaskService() — it does not know the concrete type exists
Lab — First TasksController
Build a TasksController with in-memory data. No database yet — that arrives in Session 09.
What you will build
  • A TaskItem entity
  • GET /api/tasks — return all tasks
  • GET /api/tasks/{id} — 404 if not found
  • POST /api/tasks — 201 with Location header
  • Verified end-to-end in Postman
The TaskItem Entity
Models/Entities/TaskItem.cs
public class TaskItem
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public bool IsCompleted { get; set; }
    public DateTime CreatedAt { get; set; }
}
In-memory storage for now
Store instances in a static List<TaskItem> inside your service for this session. This is replaced by Entity Framework Core and a real database in Session 09.
Endpoints to Implement
GET /api/tasks Return all tasks as a JSON array — respond 200 OK
GET /api/tasks/{id} Return a single task by id — respond 200 OK or 404 Not Found
POST /api/tasks Create a new task from the request body — respond 201 Created with a Location header pointing to the new resource
Acceptance criteria
All three endpoints tested in Postman  ·  404 returns for a missing id  ·  POST returns 201 with Location: /api/tasks/{newId}
Returning Correct HTTP Status Codes
Controllers/TasksController.cs
// 200 OK — body serialised to JSON automatically
return Ok(tasks);

// 201 Created — sets Location header to /api/tasks/{id}
return CreatedAtAction(nameof(GetById), new { id = task.Id }, task);

// 404 Not Found — empty body
return NotFound();

// 400 Bad Request — empty body or custom message
return BadRequest();

All these helper methods live on ControllerBase — your controller inherits them automatically.

Controller & Route Attributes
  • [ApiController] — enables automatic model validation, binding, and problem-detail error responses
  • [Route("api/[controller]")] — sets the base URL; [controller] resolves to the class name minus the Controller suffix
  • [HttpGet] / [HttpPost] — bind an action to an HTTP method
  • "{id}" in the route template maps to the int id parameter by name
[ApiController]
[Route("api/[controller]")]
public class TasksController : ControllerBase
{
    // GET /api/tasks
    [HttpGet]
    public IActionResult GetAll() { ... }

    // GET /api/tasks/42
    [HttpGet("{id}")]
    public IActionResult GetById(int id) { ... }

    // POST /api/tasks
    [HttpPost]
    public IActionResult Create([FromBody] TaskItem task) { ... }
}
Code Review & Discussion
Step back from the code. Think about the decisions made and challenge assumptions before they become habits.
Prompts
  • What breaks if you put database code in the controller?
  • Why depend on an interface rather than the concrete class?
Discussion Prompts
  • "What would break if you put database code directly in your controller?"
    Think about testing, readability, and what happens when you need to change the query.
  • "Why would you want an interface (ITaskService) rather than depending directly on TaskService?"
    Think about what happens in tests, and what happens if you need a second implementation later.
Session Summary
  • dotnet new webapi scaffolds a working project — Program.cs is the single entry point where services are registered and the middleware pipeline is configured
  • Every HTTP request passes through the middleware pipeline in the exact order the middleware was registered
  • Layered architecture (Controllers → Services → Repositories) separates HTTP handling, business logic, and data access into distinct, testable units
  • Dependency injection lets classes declare their dependencies as constructor parameters; the DI container resolves and provides them automatically
  • Use Scoped lifetime for services and repositories unless you have a specific reason for Singleton or Transient
  • Ok(), NotFound(), CreatedAtAction() are helper methods on ControllerBase that produce semantically correct HTTP responses
What's Next: Session 04

HTTP Methods & Status Codes

  • A full tour of GET, POST, PUT, PATCH, and DELETE — when to use each and why
  • The complete HTTP status code vocabulary: 2xx, 3xx, 4xx, 5xx
  • Idempotency and safety — properties that constrain how clients and proxies may behave
  • Expanding the TasksController with PUT, PATCH, and DELETE endpoints
Before next session
Complete today's assignment. Make sure your TasksController GET and POST endpoints pass all acceptance criteria before Session 04.
Take-Home Assignment

Create a new .NET Web API project called BooksApi that applies the layered architecture and dependency injection patterns from this session.

  • Project follows the four-folder structure: Controllers/, Services/Interfaces/, Repositories/Interfaces/, Models/Entities/
  • A Book entity with Id (int), Title (string), Author (string), and Year (int)
  • An IBooksService interface with GetAll() and GetById(int id), and a concrete in-memory BooksService implementation
  • A BooksController with GET /api/books and GET /api/books/{id} (404 if not found)
  • BooksService registered as Scoped in Program.cs and injected into the controller via constructor
Bonus
Add a POST /api/books endpoint that creates a new book and returns 201 Created with a Location header pointing to the new resource.
Questions?
Session 03  ·  First .NET Web API & Project Architecture