Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

ASP .NET Core Web API Starter Guide

This blog explains how to create REST APIs in ASP .NET Core. New project Open up Visual Studio, choose Create new project, and search for Web API: On the next screen, add the project name and click the con

ASP .NET Core Web API Starter Guide

This blog explains how to create REST APIs in ASP .NET Core.

New project

Open up Visual Studio, choose Create new project, and search for Web API:

web-api-proj-VS

On the next screen, add the project name and click the continue button until the process is finished.

project-name

Setting up the Web API controller

In the newly created project, focus on the Controllers folder. This is where REST endpoints live.

VS-project-controllers

Controller Class

Create a new empty class C#

public class TodosController
{

}

For the moment, this is a class like any other. To have API endpoints, the TodosController class must inherit the ControllerBase class built into .NET.

importing-controller-base

And have the [ApiController] attribute and the controller route:

[ApiController]
[Route("api/")] // <-- controller route
public class TodosController : ControllerBase
{

}

Endpoints

The next step is to create API endpoints that will be called from the outside. An is nothing more than a class method with some additional attributes:

Regular method:

    public int GetOne()
    {
        return 1;
    }

Endpoint method:

[ApiController]
[Route("api/")]
public class TodosController : ControllerBase
{

    [HttpGet("one")] // <-- route and action method
    public int GetOne()
    {
        return 1;
    }

}

The difference here is the [HttpGet("one")] attribute that indicates the action method ('GET') and the route name ('one').
After running the app, you can verify that the route is present in the Swagger and returns the expected result with the status code 200:
swagger-get-one

swagger-get-one-response

This also works in the browser:
https://localhost:44385/api/one

Some key points:

  • A method can return any data type, string, int, object, etc.
  • A method name can be anything.
  • Likewise, a route name can also be anything you want.
  • The route name shouldn't overlap with another route in the same controller. If there is an overlap, the method with the order of precedence will be invoked for both routes.

Using HTTP Status Codes

To use HTTP Status codes on the endpoint method, wrap the response in the ActionResult<T>.

    [HttpGet("one")]
    public ActionResult<int> GetOne()
    {
        return 1;
    }

With this in place, you can return any status code you desire.

200

    // response: 200
    [HttpGet("one")]
    public ActionResult<int> GetOne()
    {
        return Ok();
    }

The method Ok() comes from the ControllerBase class you inherited earlier and produces the 200 status code response:

// Peek inside the ControllerBase class
    public virtual OkObjectResult Ok([ActionResultObjectValue] object? value)
        => new OkObjectResult(value);

It can also accept any parameter (of primitive or complex type):

    // response: 200
    [HttpGet("one")]
    public ActionResult<int> GetOne()
    {
        return Ok(new Todo { Title = "Title" });
    }

The same applies to other status code methods.

400

    // response: 400
    [HttpGet("one")]
    public ActionResult<int> GetOne()
    {
        return BadRequest();
    }

401

    // response: 401
    [HttpGet("one")]
    public ActionResult<int> GetOne()
    {
        return Unauthorized();
    }

404

    // response: 404
    [HttpGet("one")]
    public ActionResult<int> GetOne()
    {
        return NotFound();
    }

testing-404-status

Alternative approach

You can also return the status by using the StatusCode() method:

    // response: 500
    [HttpGet("one")]
    public ActionResult<int> GetOne()
    {
        return StatusCode(500, "Optional Error Message");
    }

ActionResult & IActionResult

You might be asking yourself how, before, when I didn't use status codes, a 200 status was in the response. That's because of two things:

  • 200 status code is the default response code.
  • The use of ActionResult<T> response class

There is also the IActionResult response type that is commonly used. To demonstrate the difference, I'll create two endpoints with similar response types:

    [HttpGet("true")]
    public ActionResult<bool> GetTrue()
    {
        return true;
    }

    [HttpGet("false")]
    public IActionResult GetFalse()
    {
        return Ok(false);
    }
  • ActionResult<T> allows returning either a value of type T or any IActionResult. When returning T directly, ASP.NET Core automatically wraps it in a 200 OK response unless otherwise specified.

  • IActionResult requires returning an object that implements IActionResult. Primitive values cannot be returned directly, and the response must be wrapped (e.g., Ok(value), JsonResult, ObjectResult).

  • ActionResult<T> requires specifying the correct generic type so the framework can serialize the response and generate accurate OpenAPI metadata.

  • IActionResult does not require specifying a response type in the method signature because it represents any possible HTTP response.

Regarding the API caller, there is no difference in the response when using IActionResult or ActionResult<T>.

Dynamic route parameters

The API endpoint can accept parameters in various forms:

As Route Parameters

The parameter in the route is specified in the GetUserByID() method using the [FromRoute] attribute.

    [HttpGet("GetTodoByID/{todoID}")]
    public async ActionResult<string> GetUserByID([FromRoute] int todoID)
    {
        return $"Todo ID is: {todoID}";
    }
curl > http://localhost:44385/GetTodoByID/47

Todo ID is: 47

As Query Object

public class SearchQuery
{
    public string? Name { get; set; }
    public int? Age { get; set; }
}

When working with query objects, you only add [FromQuery] inside the Controller method, while the route stays intact.

    [HttpGet("SearchUsers")]
    public async ActionResult<string> GetUserByID([FromQuery] SearchQuery query)
    {
        if (query.Name is not null) 
        {
            return users.FirstOrDefault(u => u.Name == query.Name);
        }

        if (query.Age is not null) 
        {
            return users.FirstOrDefault(u => u.Age == query.Age);
        }

        return StatusCode(404, "User not found");
    }

As Request Body

The [FromBody] attribute is used to accept the incoming request body. Fair warning, Request Body does not apply to GET requests.

    [HttpPost("CreateUser")]
    public async Task<ActionResult<UserDTO>> CreateUser([FromBody] UserDTO dto)
    {
...
    }

As Request Headers

The [FromHeader] attribute reads headers.

    [HttpPost("SendHeaders")]
    public async Task<ActionResult<UserDTO>> SendHeaders([FromHeader] RequestHeaders headers)
    {
...
    }

You can also use multiple attributes on an endpoint at once.

Asynchronous Operations

API controllers support asynchronous processing. This is done using the Task library that is built into .NET.
Wrap the response type with the Task class Task<ActionResult<User>> GetUserByID() and prefix it with async.
Then you can use await within your controller methods:

    [HttpGet("GetUserByID/{userID}")]
    public async Task<ActionResult<User>> GetUserByID([FromRoute] int userID)
    {
        // Simulate delay
        await Task.Delay(3000);

        // Simulate calling service that returns a Task<T>
        var user = await GetUserByID(userID);

        return user;
    }

Documenting endpoints

It's always a good idea to document endpoints for other developers or Swagger users. One way to enrich endpoints is to use attributes:

    [HttpGet("GetUserByID/{userID}")]
    [ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status500InternalServerError)]
    public async Task<ActionResult<User>> GetUserByID([FromRoute] int userID)
    {
        try 
        {
            if (userID < 0) {
                var errorResponse = new GenericErrorClass { Message = "Invalid User ID!" };
                return StatusCode(400, errorResponse);
            }

            var user = await GetUserByID(userID);

            if (user is null) {
                var errorResponse = new GenericErrorClass { Message = "User not found!" };
                return StatusCode(404, errorResponse);
            }

            return user;
        }
        catch (Exception ex) 
        {
            var errorResponse = new GenericErrorClass { Message = "Something went wrong!" };
            return StatusCode(500, errorResponse);
        }
    }
public class GenericErrorClass 
{
    public string Message { get; set; }
}

In addition, you can write summary comments that will be visible in Swagger using the Swashbuckle NuGet package.

    /// <summary>
    /// Retrieves User By ID
    /// </summary>
    /// <param name="userID">User ID from users table.</param>
    /// <returns>User</returns>
    [HttpGet("GetUserByID/{userID}")]
    [ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status500InternalServerError)]
    public async Task<ActionResult<User>> GetUserByID([FromRoute] int userID)
    {
        try 
        {
            if (userID < 0) {
                var errorResponse = new GenericErrorClass { Message = "Invalid User ID!" };
                return StatusCode(400, errorResponse);
            }

            var user = await GetUserByID(userID);

            if (user is null) {
                var errorResponse = new GenericErrorClass { Message = "User not found!" };
                return StatusCode(404, errorResponse);
            }

            return user;
        }
        catch (Exception ex) 
        {
            var errorResponse = new GenericErrorClass { Message = "Something went wrong!" };
            return StatusCode(500, errorResponse);
        }
    }

Local testing

You can test your endpoints by clicking the green play button at the top. This will start the project and display all your endpoints in Swagger:

build-project

swagger-preview

Final Words

This is just a small slice of .NET Web APIs. In the following chapters, I'll dive deeper into Dependency Injection, CRUD Operations, configuration, and more.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.