Build a .NET REST API

Build a .NET REST API

 
This tutorial is based off of the freeCodeCamp youtube video .NET 5 REST API Tutorial - Build From Scratch With C# which you can find here: https://www.youtube.com/watch?v=ZXdFisA_hOY
 
The below instructions are more of an overall guide to get setup and going while also explaining some new concepts that may not be familiar to developers who havent used much .Net or even C#.
We’ll use VSCode, C#, .NET, MongoDB, Docker and Kubernetes
Note, The root material uses .NET 5 but my guide is written for .NET 6 (It shouldnt matter too much), we’ll cover basic CRUD operations, dependency injections, asynchronous code and unit testing.
 

TL;DR


  • Setup
  • GET, POST, PUT, DELETE - Steps 1 - 4
  • MongoDB
 
 

Getting Setup


Initialize project
dotnet new webapi -n <Name>
Setup self signed certificate with
dotnet dev-certs https --trust
 
In VSCode debuggger click RUN AND DEBUG and a window should open, if you have any issues check what Urls are defined in launchSettings.json just above "ASPNETCORE_ENVIRONMENT": "Development"
Then append /swagger to the end of the url, so http://localhost:5297/swagger
 
To stop the browser opening a new window everytime you restart the server, in launch.json delete serverReadyAction
Also delete WeatherForecast.cs, WeatherForecastController.cs
 
And to easily do builds, add the below to tasks.json at the bottom of the “build” task
"group": { "kind": "build", "isDefault": true }
Now you can click terminal at the top of VSCode and Run build task
 
 
 
 

Entity, Repository, Controller GET


In the root create a folder called Entities, you can also call it domains or models
Create an entity, eg item.cs Here’s inventory for items a video game character might have:
using System; namespace Catalog.Entities { public record Item { // Why record and not class? Records are immutable AND // two of them with the exact same properties will // evaluate true even if they're different instances public Guid Id { get; init; } // init is like set but with a built in constructor public string Name {get; init; } public decimal Price { get; init; } public DateTimeOffset CreatedDate { get; init; } } }
 
 
Create a temporary in-memory database. Create a new root folder Repositories, where can copy this one as an example:
using Catalog.Entities; namespace Catalog.Repositories { public class InMemItemsRepository { private readonly List<Item> items = new(){ // Data for the database // This is a new way of instancing values called 'Target typed new expression' new Item { Id = Guid.NewGuid(), Name = "Potion", Price = 9, CreatedDate = DateTimeOffset.UtcNow }, new Item { Id = Guid.NewGuid(), Name = "Iron Sword", Price = 20, CreatedDate = DateTimeOffset.UtcNow }, new Item { Id = Guid.NewGuid(), Name = "Bronze Shield", Price = 18, CreatedDate = DateTimeOffset.UtcNow } }; public IEnumerable<Item> GetItems() { // We can call this from anywhere to get all our data return items; } public Item? GetItem(Guid id) { // Use this anywhere to get an item via is Guid return items.Where(item => item.Id == id).SingleOrDefault(); // SingleOrDefault() Returns the item or null } } }
 
 
In the Controller folder, and add a new one, e.g ItemsController.cs
using Catalog.Entities; using Catalog.Repositories; using Microsoft.AspNetCore.Mvc; namespace Catalog.Controllers { [ApiController] // Adds some automatic functionality for us like errors and routing, it's called an attribute [Route("items")] // Automatically is: /items public class ItemsController : ControllerBase { // ControllerBase converts this into a controller class, ez private readonly InMemItemsRepository repository; public ItemsController() { repository = new InMemItemsRepository(); } [HttpGet] // GET /items, will call this method public IEnumerable<Item> GetItems() { var items = repository.GetItems(); return items; } } }
 
 
Now if you return to localhost:5297/swagger (port may change) and refresh, you’ll see the items GET end point. Click it to see the schema and click ‘try it out’ to receive the json payload:
// Schema [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "string", "price": 0, "createdDate": "2023-02-20T01:24:53.323Z" } ]
// Response Body [ { "id": "ecfcd1a1-775d-4d9b-a29e-e235713f14f4", "name": "Potion", "price": 9, "createdDate": "2023-02-20T01:24:53.279222+00:00" }, { "id": "8af67606-8657-4bac-8cb9-93c7143f32da", "name": "Iron Sword", "price": 20, "createdDate": "2023-02-20T01:24:53.279281+00:00" }, { "id": "23f1ae7c-be1b-493e-baf7-244905ea23ec", "name": "Bronze Shield", "price": 18, "createdDate": "2023-02-20T01:24:53.279281+00:00" } ]
You can also see it by visiting the url directly: http://localhost:5297/items
 
Now add the singular get Item to the controller, like so:
[HttpGet("{id}")] // GET /items/{id}, passing in the correct id retrieves the iem (Or null) public Item? GetItem(Guid id) { var item = repository.GetItem(id); return item; }
But wait! We generate new Guids in the temporary database every HTTP call so we could never actually search by the current values, we’ll fix that soon but first let’s add error handling for WHEN get requests fail:
[HttpGet("{id}")] public ActionResult<Item> GetItem(Guid id) { // Add ActionResult to allows us to return various status codes depending on errors var item = repository.GetItem(id); if (item is null) { return NotFound(); // Returns "404 not found" for us } return item; }
// Error status result { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4", "title": "Not Found", "status": 404, "traceId": "00-932c46c865bc7b65576ca680e1cef734-1792c858297f7a2c-00" }
 

Interface and DTOs


At this point it might be good to create an interface to represent the methods that can acquire the items, in this case the GetItems() and GetItem(). Why? because we’ll eventually convert from in-memory to a seperate database and it’s good to have the ‘contract’ solidified. Also, if you don’t know, An interface is in a way a ‘type’ that designates methods that will always be available, classes then extend it and do their own thing. The interface doesn;t give the class the logic, it merely ensures all classes that extend it MUST have these defined methods.
Here’s an example Interface called IItemsRepository.cs:
using Catalog.Entities; namespace Catalog.Repositories { public interface IItemsRepository { Item? GetItem(Guid id); IEnumerable<Item> GetItems(); } }
Then in the InMemItemsRepository.cs file we can extend the class, e.g
public class InMemItemsRepository : IItemsRepository { ...
 
Optional: Setup Singleton for the in-memory database to fix GetItem()
This issue is caused by our database setup, in the real world you wouldnt deal with it, but if you want to solve continue reading
 
Since we want our in-memory data to persist across HTTP calls, we will want to add a singleton so that it remains the same data allways from when the app starts up. In the real world, you’d use a database so this isn’t necessary. To setup a singleton, add this in Program.cs:
builder.Services.AddSingleton<IItemsRepository, InMemItemsRepository>();
Replace the instancing of the repository in `ItemController with this:
private readonly IItemsRepository repository; // Take singleton data public ItemsController(IItemsRepository repository) { this.repository = repository; // Dependency inject and bind to this class }
 
 
Data Transfer Objects are the contracts which we must ensure we fulfill for our clients so they always get the correctlt structured payload.
 
Add a new root folder Dtoswith a file ItemDto.cs and inside we’ll essentially just copy the Item.cs, but as we progress we’ll see this update:
using System; namespace Catalog.Dto { // Added Dto public record ItemDto { // Added Dto public Guid Id { get; init; } public string Name {get; init; } public decimal Price { get; init; } public DateTimeOffset CreatedDate { get; init; } } }
 
To use it, refactor ItemController.cs like so:
[HttpGet] public IEnumerable<ItemDto> GetItems() { // Dto return type var items = repository.GetItems().Select(item => new ItemDto { // Map every value Id = item.Id, Name = item.Name, Price = item.Price, CreatedDate = item.CreatedDate }); return items; }
 
We’d have to do this repeatedly on every endpoint that uses item though, so to make less repeated, create a new file in the root called Extensions.cs and put the mapping (For a single item) as shown:
using Catalog.Dto; using Catalog.Entities; namespace Catalog { public static class Extensions { public static ItemDto AsDto(this Item item) { return new ItemDto { Id = item.Id, Name = item.Name, Price = item.Price, CreatedDate = item.CreatedDate }; } } }
Then refactor the controller, ensuring the returns types match too:
[HttpGet] public IEnumerable<ItemDto> GetItems() { var items = repository.GetItems().Select(item => item.AsDto()); // Convert each to Dto return items; } [HttpGet("{id}")] public ActionResult<ItemDto> GetItem(Guid id) { // Updated return type var item = repository.GetItem(id); if (item is null) { return NotFound(); } return item.AsDto(); // Convert one to Dto }
 
Now if you return to swagger and scroll down to schemas, you’ll see it says Dto
 
 
 

POST, PUT, DELETE


For every new endpoint:
1. Add/update to the interface in it’s repository
2. Add/update a method to the class where the interface(s) is used
3. Add a new Dto with data annotations
4. Add endpoint to the controller
 
 
1. From now on, start any new endpoint at the interface IItemsRepository.cs:
public interface IItemsRepository { Item? GetItem(Guid id); IEnumerable<Item> GetItems(); void CreateItem(Item item); // New CREATE field }
 
2. You’ll notice our InMemItemsRepository.cs is broken since we haven’t implemented the create, so we can quickly add a create logic method:
public void CreateItem(Item item) { items.Add(item); }
 
3. Create a new Dto file called CreateItemDto.cs with only the values needed (from clients) to update the server, add validation as needed:
namespace Catalog.Dtos { public record CreateItemDto { [Required] // Data annotation, Returns status 400 if not provided public string Name {get; init; } [Required] [Range(1, 1000)] // Returns status 400 if our of range public decimal Price { get; init; } } }
 
4. Add a new HTTP endpoint to ItemController.cs which maps and adds the new item to the in-memory database in addition to returning not only that new resource but also a bunch of extra data including 201 created HTTP status:
[HttpPost] // POST public ActionResult<ItemDto> CreateItem(CreateItemDto itemDto) { Item item = new(){ Id = Guid.NewGuid(), // generate ID Name = itemDto.Name, Price = itemDto.Price, CreatedDate = DateTimeOffset.UtcNow // generate Date }; // Save the new resource repository.CreateItem(item); // It's convention to return the created resource // CreatedAtAction adds "201 Created" // nameof is just a title, we could add anything // A new id is simply required, we may as well use the same one // item.AsDto() is our response payload object of the created item, converted to Dto return CreatedAtAction(nameof(GetItem), new { id = item.Id}, item.AsDto()); }
 
To make a PUT request, steps 1 - 3 are very similar, the only difference is we don’t need to return the new resource like we do with update, so here’s what we can do in the controller:
[HttpPut("{id}")] // PUT /items/3fa85f64-5717-4562-b3fc-2c963f66afa6 public ActionResult UpdateItem(Guid id, UpdateItemDto itemDto) { var existingItem = repository.GetItem(id); if (existingItem is null) { return NotFound(); } // Since itemDto is a record we can use 'with' to override existing immutable values and create a new item Item updatedItem = existingItem with { Name = itemDto.Name, Price = itemDto.Price }; repository.UpdateItem(updatedItem); return NoContent(); // Returns status 200 }
 
To make a DELETE, we only need to check the resource exists and then delete it by id:
[HttpDelete("{id}")] // DELETE /items/3fa85f64-5717-4562-b3fc-2c963f66afa6 public ActionResult DeleteItem(Guid id) { var existingItem = repository.GetItem(id); if (existingItem is null) { return NotFound(); } repository.DeleteItem(id); return NoContent(); }
 
 

MongoDB


Now we want to start storing our data in. apersistent enitity, in this case we’ll use MongoDB which effectively stores .json files for us. This is known as no-SQL so our C# code can read and write to the database directly, using a driver provided by mongoDB.
Install the driver dotnet add package MongoDB.Driver
Then create a new file in the repositories folder titled MongoDbItemsRepository.cs
Initial code should look something like this:
using Catalog.Entities; using MongoDB.Driver; using MongoDB.Bson; namespace Catalog.Repositories { public class MongoDbItemsRepository : IItemsRepository { private const string databaseName = "catalogue"; // Reusable name for our one database private const string collectionName = "items"; // Reusable name for our one collection private readonly IMongoCollection<Item> itemsCollection; // Collections are how MongoDB stores the .json files private readonly FilterDefinitionBuilder<Item> filterBuilder = Builders<Item>.Filter; // Creates a reusable set of criteria that is used to select documents from a collection based on specific conditions // The below will make more sense later once mongo is configured public MongoDbItemsRepository(IMongoClient mongoClient) { // Constructor IMongoDatabase database = mongoClient.GetDatabase(databaseName); // Instance our database itemsCollection = database.GetCollection<Item>(collectionName); // Instance our collection for each of the below methods } public void CreateItem(Item item) { itemsCollection.InsertOne(item); } public void DeleteItem(Guid id) { var filter = filterBuilder.Eq(item =>item.Id, id); itemsCollection.DeleteOne(filter); } public Item? GetItem(Guid id) { var filter = filterBuilder.Eq(item =>item.Id, id); return itemsCollection.Find(filter).SingleOrDefault(); } public IEnumerable<Item> GetItems() { return itemsCollection.Find(new BsonDocument()).ToList(); } public void UpdateItem(Item item) { var filter = filterBuilder.Eq(existingItem =>existingItem.Id, item.Id); itemsCollection.ReplaceOne(filter, item); } } }
 
 

Configure MongoDB

Make sure you’ve started Docker!
and restarted your terminal
and the server is running
 
Now we’ll actually setup MongoDB within a docker container. Ensure docker is installed then run
docker run -d --rm --name mongo -p 27017:27017 -v mongodbdata:/data/db mongo
-d: Detached mode
—rm: Destroy container when we stop
-p: Exposed ports
-v: Creates a volume so when the container closes we dont lose the data
 
Run docker ps to see it running
 
Now to get our dotnet api connected to it, configure in appsettings.json:
"MongoDbSettings": { "Host": "localhost", "Port": "27017" }
Next add a new Settings folder to the repo root with a file called MondoDbSettings.cs
Here, define the same configuration as properties with a useful connection string:
namespace Catalog.Settings { public class mongoDbSettings { public string Host { get; set; } public string Port { get; set; } public string ConnectionString { get{ return $"mongodb://{Host}:{Port}"; }} } }
To initialize the settings using the appsettings configuration, return to program.cs and create a new service which does so:
builder.Services.AddSingleton<IMongoClient>(serviceProviders => { var settings = builder.Configuration.GetSection(nameof(mongoDbSettings)).Get<mongoDbSettings>(); return new MongoClient(settings.ConnectionString); }); // I honestly don't know what's happening here, but stuff is being initialized
Then if you previously used an in-memory database, just change:
builder.Services.AddSingleton<IItemsRepository, InMemItemsRepository>();
To:
builder.Services.AddSingleton<IItemsRepository, MongoDbItemsRepository>();
(Or add the ladder from scratch)
 
Then we want to add a serializer so mongoDB knows exactly what type of data is incoming, specifically for weird types like Guid and DateTimeOffset. Add these to Program.cs
// By default, the Guid type is serialized as a binary value in BSON documents, // which can be less human-readable than a string representation. // Using this custom serializer, it will appear much more friendly and is a good idea to do this for many unique formats BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); BsonSerializer.RegisterSerializer(new DateTimeOffsetSerializer(BsonType.String));
 
To visualize the data, I recomend installing the VSCode extension for MongoDB and add the connection string, e.g mongodb://localhost:27017
This lets you browse the database files
 
Don’t forget to delete InMemItemsRepository.cs!
 

Tasks, Async and Await


We can greatly speed up the speed of our API by pr0cessing multiple requests concurrently.
 
When making a method asynchronous, it’s convention to add that fact to the name, e.g in IItemsRepository.cs you would right click + rename symbol the Item GetItem( method to Task GetItemAsync (
Do that for each of the methods in that file
 
Everywhere the repo is instanced will also need to change eg by adding the Task keyword and also changing the itemsCollection method to it’s async version, for example:
// Before public void CreateItemAsync(Item item) { itemsCollection.InsertOne(item); } // After public async Task CreateItemAsync(Item item) { await itemsCollection.InsertOneAsync(item); }
Do the same for Delete, Update, GetItem and GetItems
 
Last we’ll also need to update our HTTP endpoints in ItemController.cs
For example, here’s how to update GetItems() by adding async, wrapping the return type in a Task, and awaiting then the get:
[HttpGet] // GET /items public async Task<IEnumerable<ItemDto>> GetItemsAsync() { // Note the () around the await, this is because .Select can't run immediately on a promise var items = (await repository.GetItemsAsync()).Select(item => item.AsDto()); return items; }
 
 
If you didn’t delete the InMemItemsRepository.cs you could also refactor it to handle async/await but you’ll need to do a little logic to handle actually waiting. Here’s the entire file for reference:
InMemRepository.cs Async/Await
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Catalog.Api.Entities; namespace Catalog.Api.Repositories { public class InMemItemsRepository : IItemsRepository { private readonly List<Item> items = new() { new Item { Id = Guid.NewGuid(), Name = "Potion", Price = 9, CreatedDate = DateTimeOffset.UtcNow }, new Item { Id = Guid.NewGuid(), Name = "Iron Sword", Price = 20, CreatedDate = DateTimeOffset.UtcNow }, new Item { Id = Guid.NewGuid(), Name = "Bronze Shield", Price = 18, CreatedDate = DateTimeOffset.UtcNow } }; public async Task<IEnumerable<Item>> GetItemsAsync() { return await Task.FromResult(items); } public async Task<Item> GetItemAsync(Guid id) { var item = items.Where(item => item.Id == id).SingleOrDefault(); return await Task.FromResult(item); } public async Task CreateItemAsync(Item item) { items.Add(item); await Task.CompletedTask; } public async Task UpdateItemAsync(Item item) { var index = items.FindIndex(existingItem => existingItem.Id == item.Id); items[index] = item; await Task.CompletedTask; } public async Task DeleteItemAsync(Guid id) { var index = items.FindIndex(existingItem => existingItem.Id == id); items.RemoveAt(index); await Task.CompletedTask; } } }
 
Async is always the best thing to do, but also is it not the best thing to NOT do. Generally, you’ll want these things to always be asynchronous.
 
 

Guides and practical notes, training references, and code snippets shared freely for learning and career growth.