DEV Community

Cover image for ASP .NET Core Web API Starter Guide
Mirza Leka
Mirza Leka

Posted on

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
{

}
Enter fullscreen mode Exit fullscreen mode

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
{

}
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

Endpoint method:

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

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

}
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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

200

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

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);
Enter fullscreen mode Exit fullscreen mode

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" });
    }
Enter fullscreen mode Exit fullscreen mode

The same applies to other status code methods.

400

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

401

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

404

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

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");
    }
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode
  • 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}";
    }
Enter fullscreen mode Exit fullscreen mode
curl > http://localhost:44385/GetTodoByID/47

Todo ID is: 47
Enter fullscreen mode Exit fullscreen mode

As Query Object

public class SearchQuery
{
    public string? Name { get; set; }
    public int? Age { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
Enter fullscreen mode Exit fullscreen mode

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)
    {
...
    }
Enter fullscreen mode Exit fullscreen mode

As Request Headers

The [FromHeader] attribute reads headers.

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

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;
    }
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
Enter fullscreen mode Exit fullscreen mode
public class GenericErrorClass 
{
    public string Message { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)