LEARN BUILD GROW TOGETHER
Practical knowledge for developers
DEVELOPER LEARNING HUB
Learn. Build. Grow Together.

Practical tutorials, real-world examples and interview preparation.

.NETC#SQL SERVERANGULAR
Showing posts with label Web API Core. Show all posts

Dependency Injection in .NET Core Web API – A Practical Guide with C# Examples

 

Introduction

Imagine that we are developing an Employee Management API.

Initially, our controller may contain all the logic for retrieving, creating, updating, and deleting employees.

For a small application, this might seem manageable. But as the application grows, we may need to add:

  • Database operations

  • Email notifications

  • Employee validation

  • Logging

  • External API integrations

  • Business rules

If we keep adding all this logic directly to the controller, it becomes difficult to maintain, test, and reuse.

A better approach is to separate responsibilities into different classes.

For example:

  • Controller handles HTTP requests and responses.

  • Service contains business logic.

  • Repository handles data access.

But how does the controller get an instance of the service?

We could create it manually using the new keyword. However, this creates tight coupling between classes.

.NET provides a built-in mechanism called Dependency Injection (DI) to solve this problem.

In this article, we will understand DI step by step and implement it using our Employee Management API.

1. What Is Dependency Injection?

Dependency Injection is a design technique in which an object receives the other objects it needs instead of creating them itself.

Let's understand this with a simple example.

Suppose we have an EmployeeController that needs an EmployeeService.

Without Dependency Injection:

public class EmployeesController : ControllerBase
{
    private readonly EmployeeService _employeeService;

    public EmployeesController()
    {
        _employeeService = new EmployeeService();
    }
}

Here, the controller creates the EmployeeService object directly.

This creates tight coupling because the controller is responsible for creating its dependency.

Now consider the DI approach:

public class EmployeesController : ControllerBase
{
    private readonly IEmployeeService _employeeService;

    public EmployeesController(IEmployeeService employeeService)
    {
        _employeeService = employeeService;
    }
}

Here, the controller does not create the service.

Instead, it receives the required service through its constructor.

The .NET Dependency Injection container creates and provides the appropriate implementation.

The key idea: A class should focus on its own responsibility rather than managing the creation of its dependencies.

2. What Is a Dependency?

A dependency is an object or service that another class requires to perform its work.

For example:

EmployeesController
        |
        | Depends on
        v
IEmployeeService
        |
        | Implemented by
        v
EmployeeService

In this example:

  • EmployeesController depends on IEmployeeService.

  • EmployeeService implements IEmployeeService.

  • The DI container provides an instance of EmployeeService when the controller requests IEmployeeService.

This makes the application easier to extend and test.

3. What Is Inversion of Control (IoC)?

Inversion of Control is a broader design principle in which the responsibility for controlling object creation or other application flow is transferred to an external component or framework.

Without IoC, a class may create and manage its dependencies itself.

With DI, the framework's container can manage object creation and provide dependencies.

For example:

public EmployeesController(IEmployeeService employeeService)
{
    _employeeService = employeeService;
}

The controller declares what it needs, while the DI container handles supplying the implementation.

DI vs IoC

Dependency InjectionInversion of Control
A technique for supplying dependenciesA broader design principle
Commonly implemented through constructorsCan be implemented through different mechanisms
Helps reduce tight couplingTransfers control to another component

Remember: Dependency Injection is one way to implement Inversion of Control.

4. Types of Dependency Injection

There are three commonly discussed forms of Dependency Injection.

A. Constructor Injection

Dependencies are provided through a class constructor.

public class EmployeeService
{
    private readonly ILogger<EmployeeService> _logger;

    public EmployeeService(ILogger<EmployeeService> logger)
    {
        _logger = logger;
    }
}

This is the most commonly used approach in .NET applications.

B. Property Injection

Dependencies are assigned through public properties.

public class EmployeeService
{
    public ILogger<EmployeeService>? Logger { get; set; }
}

The dependency must be assigned by some external mechanism.

The built-in .NET DI container does not automatically perform general property injection for ordinary services.

C. Method Injection

Dependencies are supplied through a method parameter.

public class EmployeeService
{
    public void ProcessEmployee(ILogger<EmployeeService> logger)
    {
        logger.LogInformation("Processing employee.");
    }
}

Method injection is useful when a dependency is required only for a particular operation.

For most application services and controllers, constructor injection is the recommended default.

5. Understanding the Built-in DI Container in .NET

.NET provides a built-in Dependency Injection container.

We use it to:

  • Register services.

  • Configure service lifetimes.

  • Resolve dependencies.

  • Manage object creation and disposal.

Service registration generally happens in Program.cs.

For example:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddScoped<IEmployeeService, EmployeeService>();

var app = builder.Build();

app.MapControllers();

app.Run();

Let's understand this line:

builder.Services.AddScoped<IEmployeeService, EmployeeService>();

It tells the DI container:

Whenever an application component requests IEmployeeService, provide an instance of EmployeeService according to the Scoped lifetime.

The controller does not need to know how the service is created.

6. Understanding Service Lifetimes

One of the most important concepts in Dependency Injection is service lifetime.

.NET provides three commonly used service lifetimes:

  1. Transient

  2. Scoped

  3. Singleton

Let's understand each one.

A. Transient

A new instance is created each time the service is requested from the container.

Registration:

builder.Services.AddTransient<IEmployeeService, EmployeeService>();

Example:

public class EmployeeService : IEmployeeService
{
}

If two separate components request the transient service, they receive different instances.

Common use cases:

  • Lightweight stateless services.

  • Small utility services.

  • Operations that do not need shared instance state.

B. Scoped

A single instance is created for each dependency injection scope.

In a typical ASP.NET Core Web API request, scoped services are shared within that request. A new request gets a different scoped instance.

Registration:

builder.Services.AddScoped<IEmployeeService, EmployeeService>();

Common use cases:

  • Business services.

  • Entity Framework Core DbContext.

  • Repository services that use a scoped DbContext.

This is a common lifetime for services used in Web APIs.

C. Singleton

A single instance is created and reused for the lifetime of the DI container.

Registration:

builder.Services.AddSingleton<IEmployeeService, EmployeeService>();

Common use cases:

  • Thread-safe shared services.

  • Application-wide caches.

  • Services that maintain shared, safely managed state.

A singleton should not directly depend on a scoped service because the scoped dependency cannot safely be used as a longer-lived object.

Service lifetime comparison

LifetimeInstance behaviorTypical use
TransientNew instance for each resolutionLightweight services
ScopedOne instance per scopeWeb request services
SingletonOne instance per containerShared application services

Practical lifetime example

Let's create a simple service to understand the difference.

public interface IOperation
{
    Guid GetOperationId();
}

public class Operation : IOperation
{
    private readonly Guid _id = Guid.NewGuid();

    public Guid GetOperationId()
    {
        return _id;
    }
}

Register it as Scoped:

builder.Services.AddScoped<IOperation, Operation>();

If two controllers or services resolve IOperation within the same request scope, they receive the same instance.

If another HTTP request comes in, it receives a new scoped instance.

Changing the registration to Transient or Singleton changes that behavior.

7. Practical Implementation: Dependency Injection in Employee Management API

Now let's implement DI in our existing Employee Management API.

In previous articles, we placed employee operations directly inside the controller.

We will now separate the business logic into a service.

Step 1: Create the Employee model

Create a folder named Models and add Employee.cs.

namespace EmployeeManagementAPI.Models;

public class Employee
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Department { get; set; } = string.Empty;

    public string Email { get; set; } = string.Empty;
}

Step 2: Create the service interface

Create a folder named Services.

Inside it, create IEmployeeService.cs.

using EmployeeManagementAPI.Models;

namespace EmployeeManagementAPI.Services;

public interface IEmployeeService
{
    List<Employee> GetAllEmployees();

    Employee? GetEmployeeById(int id);

    Employee CreateEmployee(Employee employee);
}

Why do we create an interface?

Because the controller should depend on an abstraction rather than a concrete implementation.

This makes it easier to replace the implementation later, for example, when we move from in-memory data to SQL Server.

Step 3: Create the EmployeeService implementation

Create EmployeeService.cs inside the Services folder.

using EmployeeManagementAPI.Models;

namespace EmployeeManagementAPI.Services;

public class EmployeeService : IEmployeeService
{
    private static readonly List<Employee> Employees =
    [
        new Employee
        {
            Id = 101,
            Name = "Rahul Sharma",
            Department = "IT",
            Email = "rahul@example.com"
        },
        new Employee
        {
            Id = 102,
            Name = "Amit Verma",
            Department = "Finance",
            Email = "amit@example.com"
        }
    ];

    private static int _nextId = 103;

    public List<Employee> GetAllEmployees()
    {
        return Employees;
    }

    public Employee? GetEmployeeById(int id)
    {
        return Employees.FirstOrDefault(e => e.Id == id);
    }

    public Employee CreateEmployee(Employee employee)
    {
        employee.Id = _nextId++;

        Employees.Add(employee);

        return employee;
    }
}

Here, we have moved employee-related operations into the service class.

The controller no longer needs to know how employees are stored or created.

Practical note: This example intentionally uses static in-memory data to keep the DI concept simple. It is not suitable for production. The collection is shared within the application process, is not persistent, and is not protected against concurrent modifications. We will introduce database-backed operations later.

Step 4: Register the service in Program.cs

Open Program.cs and register the service.

using EmployeeManagementAPI.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddScoped<IEmployeeService, EmployeeService>();

var app = builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

This registration connects the interface with its implementation.

Whenever a controller requests IEmployeeService, the container supplies EmployeeService.

Step 5: Inject the service into EmployeesController

Now create or update EmployeesController.cs.

using EmployeeManagementAPI.Models;
using EmployeeManagementAPI.Services;
using Microsoft.AspNetCore.Mvc;

namespace EmployeeManagementAPI.Controllers;

[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
    private readonly IEmployeeService _employeeService;

    public EmployeesController(IEmployeeService employeeService)
    {
        _employeeService = employeeService;
    }

    [HttpGet]
    public IActionResult GetAllEmployees()
    {
        var employees = _employeeService.GetAllEmployees();

        return Ok(employees);
    }

    [HttpGet("{id:int}")]
    public IActionResult GetEmployeeById(int id)
    {
        var employee = _employeeService.GetEmployeeById(id);

        if (employee == null)
        {
            return NotFound();
        }

        return Ok(employee);
    }

    [HttpPost]
    public IActionResult CreateEmployee(Employee employee)
    {
        var createdEmployee = _employeeService.CreateEmployee(employee);

        return CreatedAtAction(
            nameof(GetEmployeeById),
            new { id = createdEmployee.Id },
            createdEmployee
        );
    }
}

Let's understand the important part:

public EmployeesController(IEmployeeService employeeService)
{
    _employeeService = employeeService;
}

We are using constructor injection.

The controller declares that it requires IEmployeeService.

The DI container resolves that interface using the registration in Program.cs and provides the EmployeeService implementation.

Notice that the controller no longer creates the service using new EmployeeService().

8. Understanding the Project Structure

After implementing DI, our project structure looks like this:

EmployeeManagementAPI
│
├── Controllers
│   └── EmployeesController.cs
│
├── Models
│   └── Employee.cs
│
├── Services
│   ├── IEmployeeService.cs
│   └── EmployeeService.cs
│
├── Program.cs
│
└── EmployeeManagementAPI.csproj

Each class has a clear responsibility.

ComponentResponsibility
ControllerHandles HTTP requests and responses
InterfaceDefines the service contract
ServiceContains employee business operations
ModelRepresents employee data
Program.csRegisters application dependencies

This separation helps us maintain and extend the application as requirements grow.

9. Test the API Using Postman

Run the application and test the following endpoints.

Get all employees

GET /api/employees

Expected response:

[
  {
    "id": 101,
    "name": "Rahul Sharma",
    "department": "IT",
    "email": "rahul@example.com"
  },
  {
    "id": 102,
    "name": "Amit Verma",
    "department": "Finance",
    "email": "amit@example.com"
  }
]

Get employee by ID

GET /api/employees/101

Expected response: HTTP 200 OK with employee details.

Create an employee

POST /api/employees

Request body:

{
  "name": "Priya Singh",
  "department": "HR",
  "email": "priya@example.com"
}

Expected response: HTTP 201 Created with the newly created employee.

10. Common Dependency Injection Mistakes

Mistake 1: Forgetting to register a service

Suppose the controller requests IEmployeeService, but we forget to register it in Program.cs.

The application will fail to resolve the dependency when creating the controller.

Correct registration:

builder.Services.AddScoped<IEmployeeService, EmployeeService>();

Mistake 2: Using the wrong service lifetime

A common mistake is injecting a scoped service into a singleton service.

For example, a singleton service should not directly capture a scoped DbContext.

This can cause lifetime and state-management problems.

Mistake 3: Registering an interface with the wrong implementation

If we register the wrong class or forget to implement the interface, the application will not be configured correctly.

Always verify the interface and implementation match.

Mistake 4: Using Singleton for every service

Singleton does not mean better performance in every situation.

If a singleton stores mutable state, concurrent requests may access that state at the same time.

Choose a lifetime based on the service's responsibility and dependencies.

11. Benefits of Dependency Injection

Dependency Injection provides several advantages:

  1. Loose coupling: Classes depend on abstractions rather than creating concrete dependencies themselves.

  2. Maintainability: Responsibilities can be separated into smaller classes.

  3. Testability: Dependencies can be replaced with mock or fake implementations during unit testing.

  4. Reusability: Services can be reused by multiple controllers or other services.

  5. Centralized configuration: Service registrations and lifetimes can be managed in one place.

  6. Flexibility: Implementations can be changed without rewriting the controller.

For example, we can replace an in-memory EmployeeService with a SQL Server-backed implementation while keeping the controller dependent on the same interface.

12. Interview Questions

Q1. What is Dependency Injection?

Dependency Injection is a technique in which a class receives its dependencies from an external source instead of creating them internally.

Q2. What is Inversion of Control?

IoC is a design principle that transfers control of object creation or other behavior to an external component or framework.

Q3. What are the three service lifetimes in .NET?

Transient, Scoped, and Singleton.

Q4. What is the difference between Scoped and Singleton?

Scoped services are created once per scope, while Singleton services are shared for the lifetime of the DI container.

Q5. Which DI type is commonly used in .NET Core?

Constructor Injection is the commonly recommended approach for application services and controllers.

Q6. Why should we use interfaces with Dependency Injection?

Interfaces provide abstractions that reduce coupling and allow implementations to be replaced or substituted during testing.

Q7. Can a Singleton service depend on a Scoped service?

A singleton should not directly capture a scoped dependency because their lifetimes are different. If required, a new scope can be created using IServiceScopeFactory for a controlled operation.

Q8. Where do we register services in a .NET Core Web API?

Services are commonly registered using builder.Services in Program.cs.

Conclusion

Dependency Injection is an important part of modern .NET application development.

It helps us separate responsibilities, reduce tight coupling, and build applications that are easier to test and maintain.

In our Employee Management API, we moved employee operations from the controller into a separate service and used constructor injection to provide that service to the controller.

This is a small but important step toward building a properly structured application.

As our application grows, we can introduce repositories, database access, logging, validation, and other services without putting everything inside the controller.

What's Next?

Day 9 – DTOs and Model Validation in .NET Core Web API

In the next article, we will understand why we should not always expose database or entity models directly through API endpoints. We will create DTOs, validate incoming request data, and return meaningful validation errors.

Learn • Build • Grow Together

Understand Request and Response with JSON in .NET Core Web API – Practical Guide

 

Introduction

In Day 6, we built our first controller-based .NET Core Web API using an Employee Management example. We created endpoints to retrieve, add, update, and delete employee records.

But have you ever wondered what happens when Angular, Postman, or another application calls an API?

For example, when a client sends a request to:

GET https://localhost:7001/api/employees/101

How does the API understand the request? How does it return employee information? And why does the response usually contain JSON?

To understand API development properly, we need to understand the HTTP request-response cycle.

In this article, we will explore these concepts using practical examples in C# and .NET 8.

1. What Is an HTTP Request?

An HTTP request is a message sent by a client to a server to perform an operation or retrieve information.

A client can be:

  • An Angular or React application

  • Postman

  • A mobile application

  • Another backend service

  • A web browser

For example, suppose we want to retrieve employee information from our Employee Management API.

The client sends:

GET /api/employees/101 HTTP/1.1
Host: localhost:7001
Accept: application/json

This request contains several important parts.

Main components of an HTTP request

ComponentDescription
HTTP MethodDefines the operation, such as GET or POST
URLIdentifies the resource
HeadersProvide additional information about the request
BodyContains data sent to the server, when required

A GET request normally does not need a request body. A POST request commonly sends data in its body.

2. Understanding HTTP Request Methods

HTTP methods describe the operation the client wants to perform.

Consider our Employee Management API:

MethodEndpointPurpose
GET/api/employeesRetrieve all employees
GET/api/employees/101Retrieve one employee
POST/api/employeesCreate an employee
PUT/api/employees/101Update an employee
DELETE/api/employees/101Delete an employee

For example, when a user submits an employee creation form, the frontend sends a POST request to the API.

The request body contains the employee information.

3. What Is JSON?

JSON stands for JavaScript Object Notation.

It is a lightweight data-interchange format used to exchange information between different applications.

JSON is language-independent and widely used in REST APIs.

Example:

{
  "id": 101,
  "name": "Rahul Sharma",
  "department": "IT",
  "email": "rahul@example.com"
}

This JSON object represents an employee.

JSON supports the following data types:

Data typeExample
String"Rahul"
Number101
Booleantrue
Array[1, 2, 3]
Object{"name": "Rahul"}
Nullnull

JSON is commonly used because it is readable, compact, and supported by most programming languages.

4. Understanding Request Headers

HTTP headers provide additional information about a request or response.

For example:

POST /api/employees HTTP/1.1
Host: localhost:7001
Content-Type: application/json
Accept: application/json

Let's understand the important headers.

Content-Type

Content-Type tells the server which format is used in the request body.

For JSON:

Content-Type: application/json

This tells the API that the client is sending JSON data.

Accept

Accept tells the server which response format the client can handle.

For example:

Accept: application/json

This indicates that the client prefers a JSON response.

Authorization

The Authorization header is commonly used to send authentication credentials, such as a bearer token.

Authorization: Bearer YOUR_ACCESS_TOKEN

We will explore authentication and JWT in later articles.

Important: Content-Type and Accept are not interchangeable. One describes the request representation; the other describes the response formats the client accepts.

5. What Is an HTTP Request Body?

The request body contains the data sent by the client to the API.

For example, when creating an employee, the client may send:

{
  "name": "Amit Verma",
  "department": "Finance",
  "email": "amit@example.com"
}

The API receives this data and processes it.

In .NET Core Web API, we can use a C# model to represent the incoming JSON data.

Employee model

namespace EmployeeManagementAPI.Models
{
    public class Employee
    {
        public int Id { get; set; }

        public string Name { get; set; } = string.Empty;

        public string Department { get; set; } = string.Empty;

        public string Email { get; set; } = string.Empty;
    }
}

Receiving JSON using [FromBody]

Inside our controller:

[HttpPost]
public IActionResult CreateEmployee([FromBody] Employee employee)
{
    employee.Id = _nextId++;

    Employees.Add(employee);

    return CreatedAtAction(
        nameof(GetEmployeeById),
        new { id = employee.Id },
        employee
    );
}

Here is what happens:

  1. The client sends JSON in the request body.

  2. ASP.NET Core reads the request body.

  3. The JSON input formatter converts the JSON into an Employee C# object.

  4. The controller receives that object as the employee parameter.

  5. The application processes the request.

  6. The API returns an HTTP response.

This conversion from JSON into a C# object is called deserialization.

The [FromBody] attribute explicitly tells ASP.NET Core to bind the parameter from the request body. With [ApiController], complex types are generally inferred from the body, so explicitly adding [FromBody] is optional in this example.

6. What Is an HTTP Response?

After processing a request, the server sends an HTTP response back to the client.

An HTTP response generally contains:

  • Status code

  • Response headers

  • Response body (when applicable)

For example:

HTTP/1.1 200 OK
Content-Type: application/json

Response body:

{
  "id": 101,
  "name": "Rahul Sharma",
  "department": "IT",
  "email": "rahul@example.com"
}

The status code tells the client whether the operation succeeded or failed.

The response body contains the requested information or details about the result.

7. Important HTTP Status Codes

HTTP status codes are grouped into five categories.

CategoryMeaning
1xxInformational
2xxSuccessful
3xxRedirection
4xxClient error
5xxServer error

Some commonly used API status codes are:

Status codeMeaningExample
200OKEmployee retrieved successfully
201CreatedEmployee created successfully
204No ContentEmployee deleted successfully
400Bad RequestInvalid request data
401UnauthorizedAuthentication required or invalid
403ForbiddenUser is not allowed to perform the operation
404Not FoundEmployee does not exist
409ConflictRequest conflicts with current resource state
500Internal Server ErrorUnexpected server-side failure

Returning status codes in C#

In a controller, we can return different HTTP responses using IActionResult.

[HttpGet("{id}")]
public IActionResult GetEmployeeById(int id)
{
    var employee = Employees.FirstOrDefault(e => e.Id == id);

    if (employee == null)
    {
        return NotFound();
    }

    return Ok(employee);
}

Here:

  • Ok(employee) returns HTTP 200.

  • NotFound() returns HTTP 404.

For creating a resource, we can return:

return CreatedAtAction(
    nameof(GetEmployeeById),
    new { id = employee.Id },
    employee
);

This returns HTTP 201 Created and provides a location for retrieving the newly created resource.

8. What Is Serialization in .NET Core Web API?

Serialization means converting a C# object into JSON or another data representation.

Suppose our API has this C# object:

var employee = new Employee
{
    Id = 101,
    Name = "Rahul Sharma",
    Department = "IT",
    Email = "rahul@example.com"
};

When we return:

return Ok(employee);

ASP.NET Core uses its configured output formatter to serialize the object into JSON for the HTTP response.

Conceptually, the result looks like:

{
  "id": 101,
  "name": "Rahul Sharma",
  "department": "IT",
  "email": "rahul@example.com"
}

By default, modern ASP.NET Core Web API applications use System.Text.Json for JSON serialization and deserialization.

Manually serializing an object

We can also serialize an object in C# using System.Text.Json.

using System.Text.Json;

Employee employee = new Employee
{
    Id = 101,
    Name = "Rahul Sharma",
    Department = "IT",
    Email = "rahul@example.com"
};

string json = JsonSerializer.Serialize(employee);

Console.WriteLine(json);

Output:

{"Id":101,"Name":"Rahul Sharma","Department":"IT","Email":"rahul@example.com"}

The exact property-name formatting can depend on the serializer options used. ASP.NET Core's web defaults generally use camelCase for JSON property names.

9. What Is Deserialization in .NET Core?

Deserialization is the reverse process: converting JSON data into a C# object.

Example:

using System.Text.Json;

string json = """
{
    "id": 101,
    "name": "Rahul Sharma",
    "department": "IT",
    "email": "rahul@example.com"
}
""";

Employee? employee =
    JsonSerializer.Deserialize<Employee>(json);

Console.WriteLine(employee?.Name);

Output:

Rahul Sharma

The JSON data is converted into an Employee object, allowing us to work with strongly typed C# properties.

Remember:

  • Serialization: C# object → JSON

  • Deserialization: JSON → C# object

10. Practical Example: Complete Request and Response Flow

Let's connect the concepts using our Employee Management API.

Suppose a user wants to create a new employee from an Angular application or Postman.

Step 1: Client sends a POST request

Endpoint:

POST https://localhost:7001/api/employees

Headers:

Content-Type: application/json
Accept: application/json

Request body:

{
  "name": "Amit Verma",
  "department": "Finance",
  "email": "amit@example.com"
}

Step 2: API receives the request

The controller receives the JSON data through model binding.

[HttpPost]
public IActionResult CreateEmployee([FromBody] Employee employee)
{
    employee.Id = _nextId++;

    Employees.Add(employee);

    return CreatedAtAction(
        nameof(GetEmployeeById),
        new { id = employee.Id },
        employee
    );
}

Step 3: API processes the request

The API assigns an ID and adds the employee to its in-memory collection.

Step 4: API sends the response

Status:

201 Created

Response body:

{
  "id": 104,
  "name": "Amit Verma",
  "department": "Finance",
  "email": "amit@example.com"
}

Step 5: Client handles the response

The frontend receives the HTTP response and can display a success message or refresh the employee list.

This is the complete request-response cycle.

11. Understanding the Request-Response Lifecycle

Let's summarize what happens when a client communicates with our API.

  1. Client creates an HTTP request.

  2. The request travels to the API endpoint.

  3. ASP.NET Core processes the incoming request through its middleware pipeline.

  4. Routing selects the matching endpoint.

  5. Model binding reads and converts incoming data.

  6. The controller executes the required business operation.

  7. The API creates an HTTP response.

  8. The configured output formatter serializes the response object into JSON.

  9. The client receives the response and processes it.

The simplified flow looks like this:

Angular / Postman
       |
       | HTTP Request
       | JSON Body + Headers
       v
.NET Core Web API
       |
       v
Middleware Pipeline
       |
       v
Routing and Model Binding
       |
       v
Controller / Action
       |
       v
Application Processing
       |
       v
HTTP Response
       |
       | Status Code + JSON
       v
Angular / Postman

This is a simplified view. Real applications can also include authentication, authorization, filters, services, database access, exception handling, and other middleware.

12. Common Mistakes While Working with JSON APIs

Here are some common issues developers encounter.

Mistake 1: Incorrect Content-Type

If the client sends JSON but specifies an incorrect Content-Type, the API may not process the request as expected.

Use:

Content-Type: application/json

Mistake 2: Sending invalid JSON

Incorrect JSON:

{
  "name": "Amit",
  "department": "Finance",
}

The trailing comma makes this invalid JSON.

Correct JSON:

{
  "name": "Amit",
  "department": "Finance"
}

Mistake 3: Confusing 401 and 403

  • 401 means valid authentication has not been established.

  • 403 means the request is not permitted, even though the caller may already be authenticated.

Mistake 4: Returning 200 for every operation

A successful API does not always need to return 200.

Choose meaningful status codes:

  • 200 for a successful response with content.

  • 201 for resource creation.

  • 204 for successful operations without response content.

  • 400 for invalid requests.

  • 404 when a resource is not found.

Mistake 5: Assuming in-memory data is permanent

Our current Employee Management example uses a static in-memory collection.

The data is lost when the application restarts. Later in this series, we will connect our API to SQL Server using Entity Framework Core.

13. Interview Questions

Q1. What is JSON?

JSON is a lightweight, language-independent data format commonly used to exchange data between clients and APIs.

Q2. What is the difference between serialization and deserialization?

Serialization converts a C# object into JSON. Deserialization converts JSON into a C# object.

Q3. What is the difference between Content-Type and Accept?

Content-Type describes the format of the request or response body. Accept specifies the response formats the client can handle.

Q4. What is the purpose of the HTTP status code?

It communicates the result of an HTTP request, such as success, client error, redirection, or server error.

Q5. What is model binding in .NET Core Web API?

Model binding maps incoming HTTP request data, such as route values, query parameters, and request body data, to action method parameters or model properties.

Q6. Which JSON serializer is used by default in modern .NET Web APIs?

System.Text.Json is the default JSON serializer in modern ASP.NET Core Web API applications.

Q7. What is the difference between 200 and 201?

200 OK indicates that the request succeeded. 201 Created indicates that a resource was successfully created.

Conclusion

HTTP requests and responses are the foundation of Web API communication.

Whenever an Angular application, mobile app, or external service communicates with a .NET Core Web API, it uses HTTP to exchange information.

Understanding request methods, headers, JSON, status codes, serialization, and deserialization will help you build APIs that are easier to maintain and integrate with other applications.

We have now understood how data travels between a client and our Employee Management API.

In the next article, we will explore Dependency Injection (DI) in .NET Core Web API and understand how to manage services and dependencies without tightly coupling our application.

What's Next?

Day 8 – Dependency Injection in .NET Core Web API with Practical C# Examples.

Learn • Build • Grow Together

Build Your First .NET Core Web API Using C# – Step-by-Step Practical Guide

 

Introduction

In the previous articles, we explored the fundamentals of .NET Core Web API, HTTP methods, routing, and the difference between Minimal APIs and Controllers.

Now it's time to put that knowledge into practice.

In this article, we'll create an Employee Management Web API from scratch using C# and .NET 8.

We'll build an API that allows users to:

  • Retrieve all employees.

  • Retrieve an employee by ID.

  • Add a new employee.

  • Update employee information.

  • Delete an employee.

We'll also understand the project structure, configure the application, create a controller, implement endpoints, and test everything using Postman.

By the end of this tutorial, you'll have a basic working Web API that you can run on your local machine.

1. Prerequisites

Before starting, make sure you have the following tools installed:

ToolPurpose
.NET 8 SDKBuild and run the Web API
Visual Studio 2022 or laterDevelopment environment
C#Programming language
PostmanTest HTTP requests

You can also use Visual Studio Code with the .NET SDK.

To check whether the .NET SDK is installed, open Command Prompt or a terminal and execute:

dotnet --version

You should see an installed SDK version. For this tutorial, use a .NET 8 SDK.

You can verify the installed SDKs using:

dotnet --list-sdks

2. Create a new .NET Core Web API project

There are two convenient ways to create a Web API project.

Option A: Using Visual Studio

  1. Open Visual Studio.

  2. Select Create a new project.

  3. Search for ASP.NET Core Web API.

  4. Select the Web API template.

  5. Click Next.

  6. Enter the project name: EmployeeManagementAPI.

  7. Select the project location.

  8. Click Next.

  9. Choose .NET 8.0 as the framework.

  10. Select the Controller-based option if the wizard offers it.

  11. Click Create.

For this tutorial, we'll use Controllers rather than Minimal APIs.

Option B: Using the .NET CLI

You can also create the project directly from the terminal.

Execute:

dotnet new webapi --use-controllers -n EmployeeManagementAPI

Move into the project directory:

cd EmployeeManagementAPI

Restore the project dependencies:

dotnet restore

Build the application:

dotnet build

If the build completes successfully, your project is ready for development.

3. Understand the project structure

After creating the project, you'll see a structure similar to this:

EmployeeManagementAPI
│
├── Controllers
│   └── WeatherForecastController.cs
│
├── Properties
│   └── launchSettings.json
│
├── appsettings.json
├── appsettings.Development.json
├── EmployeeManagementAPI.csproj
└── Program.cs

Let's understand the important files.

Controllers

This folder contains controller classes that handle incoming HTTP requests.

For example, we'll create:

Controllers
└── EmployeesController.cs

Program.cs

This file contains application startup configuration, service registration, middleware, and endpoint mapping.

appsettings.json

This file contains application configuration settings, such as connection strings and logging configuration.

launchSettings.json

This file contains development launch profiles, including local application URLs and environment settings.

For this tutorial, we'll keep the project simple and focus on the files needed to build our first API.

4. Create the Employee model

A model represents the data structure used by our application.

In our Employee Management application, every employee will contain:

  • Employee ID

  • Employee Name

  • Department

  • Email Address

Create a folder named Models.

Inside it, create a file named Employee.cs.

Project structure:

EmployeeManagementAPI
│
├── Controllers
├── Models
│   └── Employee.cs
│
└── Program.cs

Add the following code:

namespace EmployeeManagementAPI.Models;

public class Employee
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Department { get; set; } = string.Empty;

    public string Email { get; set; } = string.Empty;
}

Let's understand the properties.

PropertyData typePurpose
IdintUnique employee identifier
NamestringEmployee name
DepartmentstringEmployee department
EmailstringEmployee email address

We'll use this model to represent employee information in our API.

In a later article, we'll introduce DTOs to separate API request and response models from application entities.

5. Configure Program.cs

Now open Program.cs.

This is the entry point of our application.

Replace its contents with:

var builder = WebApplication.CreateBuilder(args);

// Register controller services
builder.Services.AddControllers();

var app = builder.Build();

// Configure middleware
if (app.Environment.IsDevelopment())
{
    // Development-specific configuration can go here
}

app.UseHttpsRedirection();

// Map controller endpoints
app.MapControllers();

app.Run();

Let's understand the important statements.

1. Create the builder

var builder = WebApplication.CreateBuilder(args);

This creates the application builder and loads the default application configuration.

2. Register controllers

builder.Services.AddControllers();

This registers the services required for controller-based API development.

3. Build the application

var app = builder.Build();

This builds the application using the registered services and configuration.

4. Enable HTTPS redirection

app.UseHttpsRedirection();

This middleware redirects HTTP requests to HTTPS when the application has an appropriate HTTPS endpoint configured.

5. Map controller endpoints

app.MapControllers();

This makes attribute-routed controller endpoints available to the application's routing system.

6. Run the application

app.Run();

This starts the web application and begins accepting requests.

Our basic application configuration is now ready.

6. Create the EmployeesController

Now we'll create the main controller that handles employee-related operations.

Inside the Controllers folder, create:

EmployeesController.cs

Add the following code:

using Microsoft.AspNetCore.Mvc;
using EmployeeManagementAPI.Models;

namespace EmployeeManagementAPI.Controllers;

[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
    private static readonly List<Employee> Employees =
    [
        new Employee
        {
            Id = 101,
            Name = "Rahul",
            Department = "IT",
            Email = "rahul@example.com"
        },
        new Employee
        {
            Id = 102,
            Name = "Priya",
            Department = "HR",
            Email = "priya@example.com"
        },
        new Employee
        {
            Id = 103,
            Name = "Amit",
            Department = "Finance",
            Email = "amit@example.com"
        }
    ];

    private static int _nextId = 104;

    // GET: api/employees
    [HttpGet]
    public IActionResult GetEmployees()
    {
        return Ok(Employees);
    }

    // GET: api/employees/101
    [HttpGet("{id:int}")]
    public IActionResult GetEmployeeById(int id)
    {
        var employee = Employees.FirstOrDefault(x => x.Id == id);

        if (employee is null)
        {
            return NotFound(new
            {
                Message = "Employee not found."
            });
        }

        return Ok(employee);
    }

    // POST: api/employees
    [HttpPost]
    public IActionResult CreateEmployee(Employee employee)
    {
        employee.Id = _nextId++;

        Employees.Add(employee);

        return CreatedAtAction(
            nameof(GetEmployeeById),
            new { id = employee.Id },
            employee);
    }

    // PUT: api/employees/101
    [HttpPut("{id:int}")]
    public IActionResult UpdateEmployee(
        int id,
        Employee updatedEmployee)
    {
        var employee = Employees.FirstOrDefault(x => x.Id == id);

        if (employee is null)
        {
            return NotFound(new
            {
                Message = "Employee not found."
            });
        }

        employee.Name = updatedEmployee.Name;
        employee.Department = updatedEmployee.Department;
        employee.Email = updatedEmployee.Email;

        return Ok(employee);
    }

    // DELETE: api/employees/101
    [HttpDelete("{id:int}")]
    public IActionResult DeleteEmployee(int id)
    {
        var employee = Employees.FirstOrDefault(x => x.Id == id);

        if (employee is null)
        {
            return NotFound(new
            {
                Message = "Employee not found."
            });
        }

        Employees.Remove(employee);

        return NoContent();
    }
}

Now let's understand how each API endpoint works.

7. Understand the API endpoints

We have created five endpoints using different HTTP methods.

A. GET – Retrieve all employees

[HttpGet]
public IActionResult GetEmployees()
{
    return Ok(Employees);
}

This endpoint retrieves all employees from the collection.

Request:

GET /api/employees

Expected status:

200 OK

Response:

[
  {
    "id": 101,
    "name": "Rahul",
    "department": "IT",
    "email": "rahul@example.com"
  },
  {
    "id": 102,
    "name": "Priya",
    "department": "HR",
    "email": "priya@example.com"
  },
  {
    "id": 103,
    "name": "Amit",
    "department": "Finance",
    "email": "amit@example.com"
  }
]

The Ok() method returns an HTTP 200 response containing the employee collection.

B. GET – Retrieve an employee by ID

[HttpGet("{id:int}")]
public IActionResult GetEmployeeById(int id)
{
    var employee = Employees.FirstOrDefault(x => x.Id == id);

    if (employee is null)
    {
        return NotFound();
    }

    return Ok(employee);
}

This endpoint retrieves an individual employee using their ID.

Request:

GET /api/employees/101

Expected response:

{
  "id": 101,
  "name": "Rahul",
  "department": "IT",
  "email": "rahul@example.com"
}

If the employee does not exist, the API returns:

404 Not Found

C. POST – Create a new employee

[HttpPost]
public IActionResult CreateEmployee(Employee employee)
{
    employee.Id = _nextId++;

    Employees.Add(employee);

    return CreatedAtAction(
        nameof(GetEmployeeById),
        new { id = employee.Id },
        employee);
}

This endpoint creates a new employee.

Request:

POST /api/employees

Request body:

{
  "name": "Neha",
  "department": "Marketing",
  "email": "neha@example.com"
}

The API assigns a new ID and adds the employee to the collection.

Expected status:

201 Created

Example response:

{
  "id": 104,
  "name": "Neha",
  "department": "Marketing",
  "email": "neha@example.com"
}

CreatedAtAction() returns a 201 response and identifies the location of the newly created employee.

D. PUT – Update an existing employee

[HttpPut("{id:int}")]
public IActionResult UpdateEmployee(
    int id,
    Employee updatedEmployee)
{
    var employee = Employees.FirstOrDefault(x => x.Id == id);

    if (employee is null)
    {
        return NotFound();
    }

    employee.Name = updatedEmployee.Name;
    employee.Department = updatedEmployee.Department;
    employee.Email = updatedEmployee.Email;

    return Ok(employee);
}

This endpoint updates an existing employee.

Request:

PUT /api/employees/101

Request body:

{
  "name": "Rahul Sharma",
  "department": "Engineering",
  "email": "rahul.sharma@example.com"
}

Expected response:

{
  "id": 101,
  "name": "Rahul Sharma",
  "department": "Engineering",
  "email": "rahul.sharma@example.com"
}

The ID comes from the URL. The remaining fields are taken from the request body.

E. DELETE – Remove an employee

[HttpDelete("{id:int}")]
public IActionResult DeleteEmployee(int id)
{
    var employee = Employees.FirstOrDefault(x => x.Id == id);

    if (employee is null)
    {
        return NotFound();
    }

    Employees.Remove(employee);

    return NoContent();
}

This endpoint removes an employee from the collection.

Request:

DELETE /api/employees/101

Expected status:

204 No Content

A 204 response indicates that the operation succeeded and the server has no response body to return.

8. Run your first Web API

Now that we have created the model, controller, and application configuration, let's run the project.

Open the terminal inside your project directory and execute:

dotnet run

You should see output indicating that the application is listening on a local address.

For example:

Now listening on: https://localhost:7001
Now listening on: http://localhost:5001
Application started.

These ports are examples. Your application may use different ports depending on its launch settings.

Copy the HTTPS URL displayed in your terminal.

For example:

https://localhost:7001

Your employee API base URL will be:

https://localhost:7001/api/employees

Your first Web API is now running.

9. Test the API using Postman

Postman allows you to send HTTP requests and inspect API responses without building a frontend application.

Let's test each endpoint.

Test 1: Retrieve all employees

  • Method: GET

  • URL: https://localhost:7001/api/employees

Click Send.

You should receive a 200 OK response containing all employees.

Test 2: Retrieve an employee by ID

  • Method: GET

  • URL: https://localhost:7001/api/employees/101

Expected result: Employee Rahul.

Test 3: Create a new employee

  • Method: POST

  • URL: https://localhost:7001/api/employees

  • Select Body → raw → JSON.

Request body:

{
  "name": "Neha",
  "department": "Marketing",
  "email": "neha@example.com"
}

Click Send.

Expected result: 201 Created.

Test 4: Update an employee

  • Method: PUT

  • URL: https://localhost:7001/api/employees/101

  • Select Body → raw → JSON.

Request body:

{
  "name": "Rahul Sharma",
  "department": "Engineering",
  "email": "rahul.sharma@example.com"
}

Expected result: 200 OK.

Test 5: Delete an employee

  • Method: DELETE

  • URL: https://localhost:7001/api/employees/101

Expected result: 204 No Content.

After deletion, send the GET request again:

GET /api/employees/101

You should receive:

404 Not Found

This confirms that the employee was removed from the in-memory collection.

10. Understand the complete project structure

After completing the implementation, your project should look like this:

EmployeeManagementAPI
│
├── Controllers
│   └── EmployeesController.cs
│
├── Models
│   └── Employee.cs
│
├── Properties
│   └── launchSettings.json
│
├── appsettings.json
├── appsettings.Development.json
├── EmployeeManagementAPI.csproj
└── Program.cs

Let's understand the request flow:

Postman / Client Application
          |
          v
   HTTP Request
          |
          v
       Kestrel
          |
          v
     Middleware
          |
          v
       Routing
          |
          v
EmployeesController
          |
          v
   Employee Collection
          |
          v
    HTTP Response
          |
          v
Postman / Client Application

This connects the concepts we learned in Days 1 and 2 with the practical implementation.

11. Important limitations of this example

Our first API uses an in-memory collection to keep the implementation beginner-friendly.

However, there are some limitations you should understand.

1. Data is not permanent

The employee records exist only while the application is running. Restarting the application resets the collection to its initial values.

2. No SQL Server integration

We are not using a database yet. Database connectivity will be introduced in the upcoming Week 2 articles.

3. No separate service layer

For simplicity, the controller directly handles the collection operations. In a larger application, business logic should be moved into appropriate services.

4. No DTOs

We are using the Employee model directly for request and response data. Later, we'll understand why DTOs are important for separating API contracts from internal application models.

5. Not production-ready storage

A static in-memory list is not suitable for a multi-user production application. It is not persistent or designed for concurrent data modification.

The purpose of this tutorial is to understand the basic structure and working of a controller-based Web API before adding more advanced architecture.

12. Common mistakes beginners should avoid

1. Forgetting AddControllers()

If you are using controllers, register them in the service container:

builder.Services.AddControllers();

2. Forgetting MapControllers()

Without mapping controller endpoints, the application will not expose the controller routes:

app.MapControllers();

3. Using the wrong API URL

Check the actual port displayed when your application starts.

4. Sending invalid JSON

When using Postman, select Body → raw → JSON and provide valid JSON.

5. Using the wrong HTTP method

A GET endpoint cannot be called using POST unless an endpoint supporting POST has also been defined.

6. Expecting data to survive an application restart

Our example uses an in-memory collection. It is not connected to SQL Server.

13. Interview questions

Q1. How do you create a Web API project in .NET 8?

You can create a controller-based Web API using the Visual Studio Web API template or the CLI command:

dotnet new webapi --use-controllers -n EmployeeManagementAPI

Q2. What is the purpose of Program.cs?

Program.cs is the application's entry point. It is used to configure services, middleware, application settings, and endpoint mapping.

Q3. What is the purpose of AddControllers()?

It registers the services required for controller-based API development.

Q4. What is the purpose of MapControllers()?

It maps attribute-routed controller endpoints so incoming requests can reach the appropriate controller actions.

Q5. What is ControllerBase?

ControllerBase is a base class that provides functionality commonly used by API controllers, such as returning HTTP responses.

Q6. What is IActionResult?

IActionResult represents an HTTP action result. It allows a controller action to return different response types, such as 200 OK, 201 Created, 400 Bad Request, and 404 Not Found.

Q7. What is the difference between Ok() and CreatedAtAction()?

Ok() returns an HTTP 200 response, while CreatedAtAction() returns an HTTP 201 response and identifies the newly created resource.

Q8. Why do we use [ApiController]?

It enables API-specific conventions, including automatic model validation responses for invalid models in supported scenarios, binding-source inference, and other API behaviors.

Q9. Why is an in-memory collection not suitable for production?

It does not provide permanent storage, is not shared across multiple application instances, and requires appropriate concurrency handling. Production applications generally use persistent storage such as SQL Server.

Q10. What is the difference between a Web API and a frontend application?

A frontend application provides the user interface and sends requests, while a Web API handles backend operations and returns data or HTTP responses.

Conclusion

Congratulations! You have now built your first controller-based .NET Core Web API using C# and .NET 8.

In this tutorial, we:

  • Created a new Web API project.

  • Understood the project structure.

  • Created an Employee model.

  • Configured Program.cs.

  • Created an EmployeesController.

  • Implemented GET, POST, PUT, and DELETE endpoints.

  • Ran the API locally.

  • Tested the endpoints using Postman.

You have now moved from understanding Web API concepts to implementing a working application.

The next step is to understand how the client and server exchange structured data.

What's next?

In Day 7, we'll explore Understanding Request and Response with JSON.

We'll understand request headers, request bodies, response bodies, content types, JSON serialization, deserialization, and how data travels between an Angular application and a .NET Core Web API.


Skills Tutorials – Learn • Build • Grow Together

Continue learning with our 30-day .NET Core Web API series.

Routing and Route Parameters in .NET Core Web API with Practical C# Examples

 

Introduction

Whenever a client sends an HTTP request to a .NET Core Web API, the application needs to identify which endpoint should process that request.

For example, imagine your Employee Management application has the following requirements:

  • Retrieve all employees.

  • Retrieve an employee using their ID.

  • Retrieve employees belonging to a particular department.

  • Search employees using their name.

  • Retrieve employees based on department and location.

How does the API distinguish between these requests?

This is where Routing comes into the picture.

Routing is the mechanism that matches an incoming HTTP request to an endpoint based on information such as the URL and HTTP method.

In this article, we'll understand routing step by step and implement practical examples using Controllers and Minimal APIs.

1. What is Routing in .NET Core Web API?

Routing is the process of matching an incoming HTTP request to an appropriate endpoint.

An endpoint is the destination in your application that handles a particular request.

For example:

GET /api/employees
GET /api/employees/101
GET /api/employees/department/IT

Although all three requests belong to the Employee Management API, they perform different operations.

The routing system identifies the matching endpoint and forwards the request for execution.

Consider this simple controller:

[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
    [HttpGet]
    public IActionResult GetEmployees()
    {
        return Ok("Get all employees");
    }

    [HttpGet("{id:int}")]
    public IActionResult GetEmployeeById(int id)
    {
        return Ok($"Get employee with ID: {id}");
    }
}

Here, we have defined two endpoints:

HTTP methodURLAction
GET/api/employeesGetEmployees
GET/api/employees/101GetEmployeeById

When a client requests /api/employees/101, routing matches the second action and provides 101 as the id parameter.

2. Understanding Attribute Routing

Attribute routing allows you to define routes directly on controllers and their action methods using attributes.

It is commonly used in controller-based Web APIs.

Let's understand the two important routing attributes.

A. Route attribute

The [Route] attribute defines the route pattern for a controller or action.

Example:

[Route("api/employees")]
public class EmployeesController : ControllerBase
{
}

Now the controller's base route is:

/api/employees

You can also use a controller token:

[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
}

Here, [controller] is replaced with the controller name without the Controller suffix.

Therefore, the resulting route is:

/api/employees

B. HTTP method attributes

HTTP method attributes define which HTTP method an action accepts.

Common attributes include:

[HttpGet]
[HttpPost]
[HttpPut]
[HttpPatch]
[HttpDelete]

For example:

[HttpGet]
public IActionResult GetEmployees()
{
    return Ok("Employee list");
}

[HttpPost]
public IActionResult CreateEmployee()
{
    return Ok("Employee created");
}

Both actions use the same route, but they handle different HTTP methods.

This is useful when designing RESTful APIs.

3. What are Route Parameters?

Route parameters allow you to pass dynamic values through the URL.

Suppose you want to retrieve an employee with ID 101.

Instead of creating a separate endpoint for every employee, you can define a dynamic route:

[HttpGet("{id}")]
public IActionResult GetEmployeeById(int id)
{
    return Ok($"Employee ID: {id}");
}

Now the same endpoint can handle:

GET /api/employees/101
GET /api/employees/102
GET /api/employees/103

The framework extracts the value from the URL and binds it to the id parameter.

For example:

GET /api/employees/101

The action receives:

id = 101;

This is called route parameter binding.

Multiple route parameters

You can also define multiple parameters in a single route.

For example:

[HttpGet("{department}/{location}")]
public IActionResult GetEmployeesByDepartmentAndLocation(
    string department,
    string location)
{
    return Ok(new
    {
        Department = department,
        Location = location
    });
}

Request:

GET /api/employees/IT/Delhi

The framework binds:

department = IT
location   = Delhi

Expected response:

{
  "department": "IT",
  "location": "Delhi"
}

This approach is useful when the URL structure naturally identifies the requested resource or its hierarchy.

4. What are Route Constraints?

Sometimes you want a route parameter to accept only a particular type of value.

For example, an employee ID should be an integer.

Without a route constraint:

[HttpGet("{id}")]
public IActionResult GetEmployee(int id)
{
    return Ok(id);
}

You can explicitly define the expected parameter type using a route constraint.

Example:

[HttpGet("{id:int}")]
public IActionResult GetEmployee(int id)
{
    return Ok($"Employee ID: {id}");
}

The :int constraint ensures that the route matches only when the supplied value is an integer.

For example:

GET /api/employees/101

Matches the route.

But:

GET /api/employees/abc

Does not match this integer-constrained route.

If no other endpoint matches, the application returns a 404 Not Found response.

Common route constraints

ConstraintPurposeExample
intInteger value{id:int}
longLong integer{id:long}
guidGUID value{id:guid}
boolBoolean value{active:bool}
alphaAlphabetic characters{name:alpha}
min(value)Minimum numeric value{age:min(18)}
max(value)Maximum numeric value{age:max(60)}
range(min,max)Numeric range{age:range(18,60)}
minlength(value)Minimum string length{name:minlength(3)}
maxlength(value)Maximum string length{name:maxlength(20)}

Practical example

[HttpGet("employee/{id:guid}")]
public IActionResult GetEmployeeByGuid(Guid id)
{
    return Ok(new
    {
        EmployeeId = id
    });
}

This endpoint matches GUID values such as:

/api/employees/employee/550e8400-e29b-41d4-a716-446655440000

Route constraints are useful for selecting the correct endpoint based on the URL structure and expected value format.

Important: Route constraints are not a replacement for business validation. For example, {age:int} checks that the value is an integer, but it does not establish whether that age is valid for your business rules.

5. What are Optional Route Parameters?

Sometimes a route parameter should be optional.

For example, you may want to retrieve all employees when no department is supplied, or retrieve employees belonging to a particular department when one is provided.

You can use ? to define an optional route parameter.

Example:

[HttpGet("department/{departmentName?}")]
public IActionResult GetEmployeesByDepartment(string? departmentName)
{
    if (string.IsNullOrWhiteSpace(departmentName))
    {
        return Ok("Returning all employees");
    }

    return Ok($"Returning employees from {departmentName}");
}

Now consider these requests:

Request 1:

GET /api/employees/department

Expected result:

Returning all employees

Request 2:

GET /api/employees/department/IT

Expected result:

Returning employees from IT

The ? makes the route segment optional, while string? indicates that the parameter may be null.

In real applications, you should clearly define what happens when an optional value is missing.

6. What are Query String Parameters?

Query string parameters are values passed in the URL after the ? character.

They are commonly used for filtering, searching, sorting, and pagination.

For example:

GET /api/employees?department=IT

Here:

  • department is the query parameter.

  • IT is its value.

Unlike route parameters, query parameters are not part of the route path.

Practical example

[HttpGet("search")]
public IActionResult SearchEmployees([FromQuery] string department)
{
    return Ok(new
    {
        Department = department
    });
}

Request:

GET /api/employees/search?department=IT

The framework binds IT to the department parameter.

Multiple query parameters

You can also send multiple query parameters.

Example:

GET /api/employees/search?department=IT&location=Delhi

Controller action:

[HttpGet("search")]
public IActionResult SearchEmployees(
    [FromQuery] string department,
    [FromQuery] string location)
{
    return Ok(new
    {
        Department = department,
        Location = location
    });
}

Expected response:

{
  "department": "IT",
  "location": "Delhi"
}

Route parameters vs Query parameters

Both are useful, but they serve different purposes.

ComparisonRoute parametersQuery parameters
LocationInside URL pathAfter ?
Common purposeIdentify a resourceFilter or search resources
Example/employees/101/employees?department=IT
Required by routeDepending on route definitionUsually optional
Multiple valuesDefined as route segmentsJoined using &

Practical recommendation:

Use route parameters when identifying a specific resource.

Use query parameters when filtering or modifying how a collection is retrieved.

For example:

GET /api/employees/101
GET /api/employees?department=IT
GET /api/employees?page=1&pageSize=10

7. Routing in Minimal APIs

So far, we have explored routing through Controllers.

Minimal APIs also support routing, but instead of using controller attributes, they use endpoint mapping methods.

For example:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

// Get all employees
app.MapGet("/api/employees", () =>
{
    return Results.Ok("All employees");
});

// Get employee by ID
app.MapGet("/api/employees/{id:int}", (int id) =>
{
    return Results.Ok($"Employee ID: {id}");
});

// Get employees by department
app.MapGet("/api/employees/department/{department}", 
    (string department) =>
{
    return Results.Ok($"Department: {department}");
});

// Search employees using query parameters
app.MapGet("/api/employees/search", 
    (string department, string location) =>
{
    return Results.Ok(new
    {
        Department = department,
        Location = location
    });
});

app.Run();

Let's understand these examples.

Example 1:

app.MapGet("/api/employees", ...);

Maps a GET request to the employee collection.

Example 2:

app.MapGet("/api/employees/{id:int}", ...);

Maps a GET request with an integer route parameter.

Example 3:

app.MapGet("/api/employees/department/{department}", ...);

Maps a GET request with a department route parameter.

Example 4:

app.MapGet("/api/employees/search",
    (string department, string location) => ...);

The department and location values are inferred from query parameters because they are not route parameters or otherwise supplied from another binding source.

Request:

GET /api/employees/search?department=IT&location=Delhi

Minimal APIs provide a straightforward way to define route patterns and bind incoming values to endpoint parameters.

8. Route Groups in Minimal APIs

As the number of endpoints increases, you may notice that many endpoints share the same URL prefix.

For example:

/api/employees
/api/employees/101
/api/employees/department/IT

Instead of repeating the common prefix, Minimal APIs provide MapGroup().

Example:

var employeeGroup = app.MapGroup("/api/employees");

employeeGroup.MapGet("/", () =>
{
    return Results.Ok("All employees");
});

employeeGroup.MapGet("/{id:int}", (int id) =>
{
    return Results.Ok($"Employee ID: {id}");
});

employeeGroup.MapGet("/department/{department}", 
    (string department) =>
{
    return Results.Ok($"Department: {department}");
});

All endpoints share the /api/employees prefix.

This makes endpoint organization easier, especially when an application has multiple API modules.

Route groups can also be used to apply shared endpoint configuration, such as authorization requirements, to a group of endpoints.

9. How does .NET Core Web API select the correct route?

Imagine you have the following endpoints:

[HttpGet("{id:int}")]
public IActionResult GetEmployeeById(int id)
{
    return Ok($"Employee ID: {id}");
}

[HttpGet("search")]
public IActionResult SearchEmployees()
{
    return Ok("Search employees");
}

Now consider these requests:

GET /api/employees/101
GET /api/employees/search

The routing system considers the available route patterns and their constraints to identify the appropriate endpoint.

The first request matches the integer route.

The second request matches the literal search route.

Endpoint selection is based on route matching, HTTP method, constraints, and endpoint metadata—not simply on the order in which controller methods appear in the file.

If no suitable endpoint matches, the application generally returns 404 Not Found.

If a path matches an endpoint but the HTTP method is not supported, the application can return 405 Method Not Allowed.

This distinction is useful when debugging routing problems.

10. Common routing mistakes developers should avoid

1. Forgetting the controller route prefix

If your controller has:

[Route("api/[controller]")]

Remember that the resulting URL includes the api prefix and controller name.

For EmployeesController, the route starts with /api/employees.

2. Using route parameters when query parameters are more suitable

For filtering a collection, a query string is often clearer:

GET /api/employees?department=IT

Instead of creating a separate route for every possible filter.

3. Forgetting route constraints

If an endpoint expects an integer ID, adding {id:int} makes the intended route requirement explicit.

4. Confusing route parameters with query parameters

These are different request values:

GET /api/employees/101
GET /api/employees?id=101

The first uses a route parameter. The second uses a query parameter.

5. Creating ambiguous routes

Avoid defining multiple endpoints with overlapping route patterns and the same HTTP method if the framework cannot distinguish them.

Design routes with clear and predictable patterns.

6. Using route constraints as business validation

An integer constraint verifies the URL value's format. It does not verify whether an employee exists or whether the value satisfies business rules.

11. Interview questions

Q1. What is routing in .NET Core Web API?

Routing is the process of matching incoming HTTP requests to the appropriate endpoints based on information such as the URL and HTTP method.

Q2. What is attribute routing?

Attribute routing allows developers to define routes directly on controllers and action methods using attributes such as [Route], [HttpGet], and [HttpPost].

Q3. What is the difference between route parameters and query parameters?

Route parameters are part of the URL path and are commonly used to identify resources. Query parameters appear after the ? character and are commonly used for filtering, searching, and pagination.

Q4. What is a route constraint?

A route constraint restricts the values that can match a route parameter. For example, {id:int} matches integer values.

Q5. How do you define an optional route parameter?

You can use ? in the route template.

Example:

[HttpGet("department/{name?}")]

Q6. What is the purpose of [FromQuery]?

[FromQuery] explicitly tells the framework to bind a parameter from the query string.

Q7. What is the purpose of [controller] in routing?

It is a route token that is replaced with the controller name without the Controller suffix.

Q8. What is MapGroup() in Minimal APIs?

MapGroup() allows related endpoints to share a common route prefix and endpoint configuration.

Q9. What is the difference between 404 and 405?

404 Not Found indicates that no matching resource or endpoint was found. 405 Method Not Allowed indicates that the target resource does not support the requested HTTP method.

Q10. Can we use both route parameters and query parameters in the same endpoint?

Yes. For example:

GET /api/employees/101?includeDepartment=true

Here, 101 identifies the employee, while includeDepartment is a query parameter.

Conclusion

Routing is one of the fundamental concepts in .NET Core Web API development.

It helps the application identify the correct endpoint for every incoming request.

In this article, we explored:

  • Attribute routing

  • Route parameters

  • Multiple route parameters

  • Route constraints

  • Optional route parameters

  • Query string parameters

  • Routing in Minimal APIs

  • Route groups

We also learned how route parameters and query parameters are used for different purposes when designing API URLs.

Understanding these concepts will help you create clean, predictable, and maintainable API endpoints.

What's next?

In Day 6, we'll move from individual routing concepts to practical implementation.

We'll Build Your First .NET Core Web API step by step, create a new project, configure the application, implement employee endpoints, run the API, and test it using an API client.

We'll bring together the concepts we've learned so far and build our first complete working API project.


Skills Tutorials – Learn • Build • Grow Together

Continue learning with our 30-day .NET Core Web API series.

Minimal APIs vs Controllers in .NET Core Web API: Differences with Practical Examples

 

Introduction

When building a .NET Core Web API, one important architectural decision is how you want to define and organize your API endpoints.

.NET provides two commonly used approaches:

  1. Minimal APIs

  2. Controller-based APIs

Both approaches allow you to create HTTP endpoints, receive requests, execute application logic, and return responses to clients.

But their programming styles and organization are different.

Imagine you are developing an Employee Management application. You need endpoints to retrieve employees, create new employees, update employee details, and delete employee records.

Should you implement these endpoints directly in Program.cs using Minimal APIs, or should you create an EmployeesController?

Let's understand both approaches with practical C# examples.

1. What are Minimal APIs?

Minimal APIs were introduced in .NET 6 to provide a simplified way of building HTTP APIs with less boilerplate code.

Instead of creating separate controller classes with attributes, you can define API endpoints directly using methods such as:

  • MapGet()

  • MapPost()

  • MapPut()

  • MapPatch()

  • MapDelete()

These methods map incoming HTTP requests to the code responsible for processing them.

Minimal APIs are particularly useful for lightweight services, small applications, microservices, and APIs where a straightforward endpoint-based approach is sufficient.

Example: Creating an API using Minimal APIs

Create a new Web API project using the following command:

dotnet new web -n EmployeeMinimalApi

Open Program.cs.

Replace its contents with:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

var employees = new List<Employee>
{
    new Employee(101, "Rahul", "IT"),
    new Employee(102, "Priya", "HR")
};

// Get all employees
app.MapGet("/api/employees", () =>
{
    return Results.Ok(employees);
});

// Get employee by ID
app.MapGet("/api/employees/{id:int}", (int id) =>
{
    var employee = employees.FirstOrDefault(x => x.Id == id);

    return employee is not null
        ? Results.Ok(employee)
        : Results.NotFound();
});

// Create employee
app.MapPost("/api/employees", (Employee employee) =>
{
    var newId = employees.Count == 0
        ? 1
        : employees.Max(x => x.Id) + 1;

    var newEmployee = employee with { Id = newId };

    employees.Add(newEmployee);

    return Results.Created(
        $"/api/employees/{newEmployee.Id}",
        newEmployee);
});

// Update employee
app.MapPut("/api/employees/{id:int}",
    (int id, Employee updatedEmployee) =>
{
    var index = employees.FindIndex(x => x.Id == id);

    if (index == -1)
    {
        return Results.NotFound();
    }

    employees[index] = updatedEmployee with { Id = id };

    return Results.Ok(employees[index]);
});

// Delete employee
app.MapDelete("/api/employees/{id:int}", (int id) =>
{
    var employee = employees.FirstOrDefault(x => x.Id == id);

    if (employee is null)
    {
        return Results.NotFound();
    }

    employees.Remove(employee);

    return Results.NoContent();
});

app.Run();

public record Employee(int Id, string Name, string Department);

Let's understand what is happening here.

Step 1: Create the application

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

This creates and builds the web application.

Step 2: Define the endpoints

app.MapGet("/api/employees", () =>
{
    return Results.Ok(employees);
});

This maps an HTTP GET request to /api/employees.

When a client calls this endpoint, the application returns the employee collection.

Step 3: Handle route parameters

app.MapGet("/api/employees/{id:int}", (int id) =>
{
    var employee = employees.FirstOrDefault(x => x.Id == id);

    return employee is not null
        ? Results.Ok(employee)
        : Results.NotFound();
});

Here, the employee ID comes from the URL and is provided to the endpoint handler.

Step 4: Return HTTP responses

Minimal APIs provide the Results class to create HTTP responses.

Examples:

Results.Ok(data);
Results.Created(location, data);
Results.NotFound();
Results.NoContent();

These results represent HTTP responses such as 200, 201, 404, and 204.

Testing Minimal API endpoints

Run your application and test the following requests using Postman.

HTTP methodEndpointPurpose
GET/api/employeesGet all employees
GET/api/employees/101Get employee by ID
POST/api/employeesCreate employee
PUT/api/employees/101Update employee
DELETE/api/employees/101Delete employee

For POST, use this JSON request body:

{
  "id": 0,
  "name": "Amit",
  "department": "Finance"
}

The API assigns the new ID.

The example uses an in-memory collection, so records are not permanently stored. Also, the PATCH operation is not implemented in this particular example; it can be added using MapPatch().

2. What are Controller-based APIs?

Controller-based APIs use controller classes to organize HTTP endpoints.

A controller is a class that typically inherits from ControllerBase and contains action methods that handle incoming requests.

Instead of defining all endpoints in Program.cs, you organize them into separate controller classes.

For example:

EmployeeManagement.Api
│
├── Controllers
│   └── EmployeesController.cs
│
├── Models
│   └── Employee.cs
│
└── Program.cs

This approach is commonly used in enterprise applications where APIs contain multiple endpoints and require clear separation of responsibilities.

Example: Creating an API using Controllers

Create a new Web API project:

dotnet new webapi --use-controllers -n EmployeeControllerApi

Open Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

Here:

  • AddControllers() registers the controller services.

  • MapControllers() maps controller endpoints into the application's routing system.

Now create a folder named Controllers and add EmployeesController.cs.

using Microsoft.AspNetCore.Mvc;

namespace EmployeeControllerApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
    private static readonly List<Employee> Employees =
    [
        new Employee
        {
            Id = 101,
            Name = "Rahul",
            Department = "IT"
        },
        new Employee
        {
            Id = 102,
            Name = "Priya",
            Department = "HR"
        }
    ];

    [HttpGet]
    public IActionResult GetEmployees()
    {
        return Ok(Employees);
    }

    [HttpGet("{id:int}")]
    public IActionResult GetEmployeeById(int id)
    {
        var employee = Employees.FirstOrDefault(x => x.Id == id);

        if (employee is null)
        {
            return NotFound();
        }

        return Ok(employee);
    }

    [HttpPost]
    public IActionResult CreateEmployee(Employee employee)
    {
        employee.Id = Employees.Count == 0
            ? 1
            : Employees.Max(x => x.Id) + 1;

        Employees.Add(employee);

        return CreatedAtAction(
            nameof(GetEmployeeById),
            new { id = employee.Id },
            employee);
    }

    [HttpPut("{id:int}")]
    public IActionResult UpdateEmployee(
        int id,
        Employee updatedEmployee)
    {
        var employee = Employees.FirstOrDefault(x => x.Id == id);

        if (employee is null)
        {
            return NotFound();
        }

        employee.Name = updatedEmployee.Name;
        employee.Department = updatedEmployee.Department;

        return Ok(employee);
    }

    [HttpDelete("{id:int}")]
    public IActionResult DeleteEmployee(int id)
    {
        var employee = Employees.FirstOrDefault(x => x.Id == id);

        if (employee is null)
        {
            return NotFound();
        }

        Employees.Remove(employee);

        return NoContent();
    }
}

Create the Employee.cs model:

namespace EmployeeControllerApi;

public class Employee
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Department { get; set; } = string.Empty;
}

Understanding the controller code

Let's understand the important parts.

1. Controller declaration

[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
  • [ApiController] enables API-specific conventions and behaviors, including automatic model validation responses in supported scenarios.

  • [Route] defines the route pattern.

  • [controller] is replaced by the controller name without the Controller suffix.

  • ControllerBase provides functionality commonly needed when building APIs.

Therefore, the base route becomes:

/api/employees

2. HTTP GET action

[HttpGet]
public IActionResult GetEmployees()
{
    return Ok(Employees);
}

The [HttpGet] attribute maps GET requests to this action.

3. HTTP POST action

[HttpPost]
public IActionResult CreateEmployee(Employee employee)
{
    // Create employee
}

The incoming request body is bound to the Employee parameter.

4. HTTP PUT action

[HttpPut("{id:int}")]
public IActionResult UpdateEmployee(int id, Employee updatedEmployee)

This action accepts the employee ID from the URL and employee information from the request body.

5. HTTP DELETE action

[HttpDelete("{id:int}")]
public IActionResult DeleteEmployee(int id)

This action identifies the employee using the route ID and removes the corresponding record.

The controller approach organizes multiple endpoints within a dedicated class rather than placing all endpoint definitions inside Program.cs.

3. Minimal APIs vs Controllers: Key differences

Now that we have implemented both approaches, let's compare them.

FeatureMinimal APIsControllers
Programming styleEndpoint-basedClass and action-based
Code structureCan be compactUses controller classes
RoutingMapGet(), MapPost() etc.Routing attributes
Request handlingEndpoint handlersController action methods
HTTP responsesResults and typed resultsIActionResult, ActionResult<T>
OrganizationFlexible; can use route groups and extension methodsNaturally organized by controllers
FiltersEndpoint filtersMVC filters and other MVC extensibility
Model validationCan be implemented using endpoint logic, validation libraries, and framework features[ApiController] provides automatic model validation responses for invalid models
Dependency injectionSupportedSupported
Suitable forLightweight endpoints and focused servicesStructured APIs and controller-oriented applications

Important: Both approaches support routing, dependency injection, authentication, authorization, and other application capabilities. The choice is mainly about programming style, organization, and the features your application needs.

4. Advantages of Minimal APIs

Minimal APIs provide several benefits.

Less boilerplate code

You can create endpoints without defining a separate controller class for every API group.

Simple endpoint definitions

HTTP methods and their handlers can be defined directly using methods such as MapGet() and MapPost().

Useful for small services

When an application has a limited number of endpoints, Minimal APIs can provide a straightforward implementation.

Support for route groups

You can group related endpoints together and apply shared route prefixes or endpoint configuration.

For example:

var employeeGroup = app.MapGroup("/api/employees");

employeeGroup.MapGet("/", () => "Get employees");

employeeGroup.MapGet("/{id:int}", (int id) =>
    $"Get employee {id}");

Both endpoints share the /api/employees route prefix.

This helps organize Minimal APIs as the number of endpoints grows.

5. Advantages of Controller-based APIs

Controllers are useful when you want an organized structure for larger applications.

Clear separation of endpoints

Related API operations can be grouped inside a controller class.

For example:

Controllers
│
├── EmployeesController.cs
├── DepartmentsController.cs
├── ProductsController.cs
└── OrdersController.cs

Attribute-based routing

You can define routes using attributes such as:

[HttpGet]
[HttpPost]
[HttpPut("{id}")]
[HttpDelete("{id}")]

MVC features

Controller-based APIs support MVC features such as:

  • Action filters

  • Model binding

  • Model validation

  • Controller conventions

  • MVC-specific extensibility

Familiar structure for enterprise applications

Teams already working with MVC and controller-based architectures may find this approach easier to maintain and extend.

6. Can Minimal APIs support Dependency Injection?

Yes. Both Minimal APIs and Controllers support Dependency Injection.

Dependency Injection allows you to provide required services to application components rather than creating those dependencies manually everywhere.

For example, imagine you have an IEmployeeService.

First, register the service:

builder.Services.AddScoped<IEmployeeService, EmployeeService>();

In a Minimal API, you can inject it into an endpoint handler:

app.MapGet("/api/employees",
    (IEmployeeService employeeService) =>
{
    return employeeService.GetAllEmployees();
});

In a controller, you can inject it through the constructor:

[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
    private readonly IEmployeeService _employeeService;

    public EmployeesController(
        IEmployeeService employeeService)
    {
        _employeeService = employeeService;
    }

    [HttpGet]
    public IActionResult GetEmployees()
    {
        return Ok(_employeeService.GetAllEmployees());
    }
}

The important point is that Dependency Injection is not exclusive to either approach.

We'll explore Dependency Injection in detail in Day 8 of this series.

7. When should you use Minimal APIs?

Minimal APIs are a good choice when:

  • You are building a small API service.

  • You need a limited number of HTTP endpoints.

  • You are developing lightweight microservices.

  • You want a straightforward endpoint-based programming model.

  • Your team prefers explicit endpoint registration and organization.

For example, imagine an internal service that exposes only a few endpoints to retrieve application configuration or check service health.

Minimal APIs may be a suitable choice.

However, Minimal APIs can also be used in larger applications. You can organize them with route groups, extension methods, and separate endpoint-registration files.

8. When should you use Controllers?

Controllers are a good choice when:

  • Your application has many related API endpoints.

  • You prefer organizing endpoints into controller classes.

  • You need MVC-specific features such as action filters.

  • Your team already follows a controller-based architecture.

  • You want a familiar structure for a larger API codebase.

For example, an Employee Management application may eventually contain:

  • Employee management

  • Department management

  • Attendance management

  • Payroll management

  • Reporting

  • Role-based access control

A controller-based structure can make it easier to organize these related areas.

Again, this does not mean Minimal APIs cannot handle such an application. Both approaches can be used with a well-designed architecture.

9. Which approach is better: Minimal APIs or Controllers?

There is no single answer that applies to every application.

The decision depends on the application requirements, team experience, architecture, and the framework features you need.

Consider this practical comparison:

Scenario 1: Small internal service

You need five endpoints to expose application health and configuration information.

Minimal APIs may be a convenient choice because they allow you to define the endpoints with relatively little setup.

Scenario 2: Large Employee Management system

You have multiple business modules, several developers, complex request handling, and MVC-specific requirements.

Controllers may provide a more familiar organizational structure.

Scenario 3: Existing controller-based application

Your organization already has a controller-based Web API with established conventions and reusable components.

Continuing with controllers may be the most practical decision. You do not need to rewrite the application simply because Minimal APIs are available.

My recommendation: Choose the approach based on actual requirements, not just the amount of code. A maintainable API with clear responsibilities is more important than choosing the approach with the fewest lines.

10. Important points developers should remember

Before moving forward, keep these points in mind:

  1. Minimal APIs were introduced in .NET 6.

  2. Both Minimal APIs and Controllers can build HTTP APIs.

  3. Both approaches support Dependency Injection.

  4. Minimal APIs use endpoint mapping methods such as MapGet() and MapPost().

  5. Controllers use controller classes, action methods, and routing attributes.

  6. Minimal APIs support route groups and endpoint filters.

  7. Controllers support MVC features such as action filters and controller conventions.

  8. Neither approach automatically makes your API secure or production-ready. You must configure the appropriate security, validation, error handling, and monitoring features.

11. Interview questions

Q1. What are Minimal APIs in .NET?

Minimal APIs provide a simplified way to build HTTP APIs by defining endpoints directly using methods such as MapGet(), MapPost(), MapPut(), and MapDelete().

Q2. In which .NET version were Minimal APIs introduced?

Minimal APIs were introduced in .NET 6.

Q3. What is the difference between Minimal APIs and Controllers?

Minimal APIs define endpoints using endpoint mapping methods, while Controllers organize API endpoints inside classes containing action methods.

Q4. Can we use Dependency Injection in Minimal APIs?

Yes. Minimal APIs support Dependency Injection. Registered services can be injected directly into endpoint handlers.

Q5. Do Minimal APIs support authentication and authorization?

Yes. Minimal APIs support authentication and authorization through the ASP.NET Core authentication and authorization infrastructure.

Q6. What is MapGet()?

MapGet() maps an HTTP GET request to an endpoint handler.

Q7. What is MapControllers()?

MapControllers() maps controller action endpoints into the application's routing system.

Q8. Can Minimal APIs be used for large applications?

Yes. Minimal APIs can be organized using route groups, extension methods, and separate endpoint-registration files. They are not limited to small applications.

Q9. Can we use Minimal APIs and Controllers in the same project?

Yes. A .NET application can contain both Minimal API endpoints and controller-based endpoints.

Q10. Which approach is faster?

Performance depends on the actual application, workload, configuration, and implementation. You should benchmark your application's real requirements rather than assuming one approach is always faster.

Conclusion

Minimal APIs and Controller-based APIs are two supported approaches for building modern .NET Core Web APIs.

Minimal APIs provide a straightforward endpoint-based programming model, while Controllers provide a class-based structure with MVC-specific features.

In this article, we implemented the same Employee Management functionality using both approaches and compared their structure, advantages, and use cases.

The most important takeaway is that both approaches can build maintainable and production-ready APIs when designed properly.

What's next?

In Day 5, we'll explore Routing and Route Parameters in .NET Core Web API.

We'll understand how API URLs are mapped to endpoints, how route parameters work, how to use optional parameters and constraints, and how to design clean and predictable API routes with practical C# examples.


Skills Tutorials – Learn • Build • Grow Together

Continue learning with our 30-day .NET Core Web API series.