In part 3 of this tutorial I will show how to enable the backend API for the “Locations In Your Pocket”-application so that location items can be persisted in a database. I will also present how the REST API is designed and implemented.
Overview
Part 2 of this blog series was a walkthrough of the frontend Angular application “Locations In Your Pocket”. It was using fake data stored in-memory in the browser. To make the application complete, we will now connect it to a REST API that stores the location items in a database.
I have used the ASP.NET Core WebAPI 3.1 API-template in Visual Studio for creating the base for the API. You can create a project with this template from the console with the .NET Core CLI as well:
dotnet new webapi
After removing the default example code, I have added a new API controller that supports POST, PUT, GET and DELETE of Location entities with some basic validations. I’ve create a LocationsRepository-implementation that uses Sqlite in a separate .NET Standard library. This repository is injected into the controller.
Test the Angular application with the back-end API
Build and start the REST API
In Visual Studio 2019, open Server/LocPoc.sln from the git repository. Build the solution and set the LocPoc.Api-project as startup project. Start the debugger. It should start a process that listens on port 5001 on localhost.
With .NET Core CLI you can do this from a console instead (from the loc-poc/Server folder):
dotnet build dotnet run --project LocPoc.Api
Configure the Angular application to use the REST API
Use the Angular application that is described in Part 2 of this blog series. Open app-settings.json in the Angular project. Set “useFakeData”: false and check that the apiPort corresponds to the port that the API is listening to (if you want to use the map view, remember to configure the googleAPIKey as well):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"apiUrl": "https://localhost", | |
"apiPort": "5001", | |
"useFakeData": false, | |
"useMap": true, | |
"googleAPIKey": "ABzaDuAQbFxSalDaWfIAB7DEZAh730AFGGGlpg" | |
} |
Build and start the Angular app from the Clients/LocPocAngular directory:
yarn install yarn startSSL
Use a web browser and navigate to https://localhost:4300
This should show an empty list as your database is empty.
Use the New-button for adding a couple of location items.
If you restart the Server and the Angular application, your created data should remain as they have been persisted in a database. The database is a file, locpoc.db, that was created the first time the API was started.
Implementation details
The solution for the API contains these three projects:
- LocPoc.Api – The ASP.NET Core application.
- LocPoc.Contracts – A library that contains the interfaces and entities.
- LocPoc.Repository.Sqlite – A library that contains an Sqlite implementation of the repository interface.
Contracts
I prefer to have interfaces and the entities they use in a separate assembly so that code that need the interfaces don’t get any unwanted implementation details dependencies when referencing the interfaces. For this project it’s a bit overkill (as it has just one interface and one entity class), but to me it is good practice and makes it easier to extend the application later without creating a big ball of mud. There is a .NET Standard Assembly called LocPoc.Contracts.
The first repository that I used was based on synchronous methods:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
namespace LocPoc.Contracts | |
{ | |
public interface ILocationsRepository | |
{ | |
IEnumerable<Location> GetAll(); | |
Location Get(string id); | |
Location Create(Location location); | |
Location Update(Location location); | |
void Delete(string id); | |
} | |
} |
But I later replaced it with an async version for better scaling:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.Threading.Tasks; | |
namespace LocPoc.Contracts | |
{ | |
public interface ILocationsRepositoryAsync | |
{ | |
Task<IEnumerable<Location>> GetAllAsync(); | |
Task<Location> GetAsync(string id); | |
Task<Location> CreateAsync(Location location); | |
Task<Location> UpdateAsync(Location location); | |
Task DeleteAsync(string id); | |
} | |
} |
By using async methods we can use the await keyword on I/O-operations which can free up API-threads while I/O is working. For this small application it does not really matter but it was a fun exercise so I went async with the repository- and the controller implementation (see sections below).
Anyway, regardless of if the repository interface is synchronous or asynchronous, we can add different repository implementations (different types of data storage) that adhere to these interfaces.
Controller
The LocationsController handles the http requests that comes to the API with “api/locations” as part of the URL. Notice that it only depends on the repository interface. The controller code has no idea about how the data storage is actually implemented (whether it is Sqlite or any other type of data storage). The configuration part of the ASP.NET Core application determines what implementation should be bound to the repository interface and the dependency injection mechanism sees to that the configured implementation is passed to the constructor of the API controller when it is created.
I use a dedicated Data-Transfer-Object class (DTOs.Location) as input and output for the controller. The reason for this is that I don’t want to expose the internal Location entity outside the API (the internal Location entity could have extra properties that the externally exposed LocationDto should not have). For this small project it does not really matter, but again, it is good a practice if the project grows. I could have had separate DTO-classes for the create/update/delete/get methods. E.g., Location.Id does not have any effect when creating a new item while and Id-property is useful in a response from a Get-request. But for keeping it simple I just use one single DTO-class.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace LocPoc.Api.Controllers | |
{ | |
[Route("api/[controller]")] | |
[ApiController] | |
public class LocationsController : ControllerBase | |
{ | |
ILocationsRepositoryAsync _locationsRepository; | |
public LocationsController(ILocationsRepositoryAsync locationsRepository) | |
{ | |
_locationsRepository = locationsRepository ?? throw new ArgumentNullException(nameof(locationsRepository)); | |
} | |
/// <summary> | |
/// Gets all location items | |
/// </summary> | |
/// <returns>A collection of location items</returns> | |
/// <response code="200">Returns all location items</response> | |
[HttpGet] | |
public async Task<ActionResult<IEnumerable<DTOs.Location>>> Get() | |
{ | |
var locations = await _locationsRepository.GetAllAsync(); | |
var locationDTOs = locations.Select(loc => loc.ToLocationDto()); | |
return Ok(locationDTOs); | |
} | |
/// <summary> | |
/// Gets a location item by id | |
/// </summary> | |
/// <returns>A location item</returns> | |
/// <response code="200">Returns the requested location item</response> | |
[ProducesResponseType(StatusCodes.Status200OK)] | |
[ProducesResponseType(StatusCodes.Status404NotFound)] | |
[HttpGet("{id}", Name = "Get")] | |
public async Task<ActionResult<DTOs.Location>> Get(string id) | |
{ | |
var existingLoc = await _locationsRepository.GetAsync(id); | |
if (existingLoc == null) | |
return NotFound(); | |
return existingLoc.ToLocationDto(); | |
} | |
/// <summary> | |
/// Creates a new location item | |
/// </summary> | |
/// <returns>The created location item</returns> | |
/// <response code="200">Returns the created location item</response> | |
[ProducesResponseType(StatusCodes.Status200OK)] | |
[ProducesResponseType(StatusCodes.Status400BadRequest)] | |
[HttpPost] | |
public async Task<ActionResult<DTOs.Location>> Create([FromBody] DTOs.Location locationDto) | |
{ | |
var location = locationDto.ToLocation(); | |
var createdLoc = await _locationsRepository.CreateAsync(location); | |
return Created("location", createdLoc.ToLocationDto()); | |
} | |
/// <summary> | |
/// Updates an existing location item | |
/// </summary> | |
/// <returns>The updated location item</returns> | |
/// <response code="200">Returns the updated location item</response> | |
[ProducesResponseType(StatusCodes.Status200OK)] | |
[ProducesResponseType(StatusCodes.Status404NotFound)] | |
[ProducesResponseType(StatusCodes.Status400BadRequest)] | |
[HttpPut("{id}")] | |
public async Task<ActionResult<DTOs.Location>> Update(string id, [FromBody] DTOs.Location locationDto) | |
{ | |
locationDto.Id = id; | |
var location = locationDto.ToLocation(); | |
var existingLoc = await _locationsRepository.GetAsync(location.Id); | |
if (existingLoc == null) | |
return NotFound(); | |
var updatedLoc = await _locationsRepository.UpdateAsync(location); | |
return Ok(updatedLoc.ToLocationDto()); | |
} | |
/// <summary> | |
/// Deletes a location item | |
/// </summary> | |
/// <returns></returns> | |
[ProducesResponseType(StatusCodes.Status200OK)] | |
[ProducesResponseType(StatusCodes.Status404NotFound)] | |
[HttpDelete("{id}")] | |
public async Task<IActionResult> Delete(string id) | |
{ | |
var existingLoc = await _locationsRepository.GetAsync(id); | |
if (existingLoc == null) | |
return NotFound(); | |
await _locationsRepository.DeleteAsync(id); | |
return NoContent(); | |
} | |
} | |
} |
Repository
The concrete repository implementation that we will use operates on an Sqlite database. Sqlite is a light-weight database that keeps the schema and data in a single binary file on disk. It is not a client/server database. The operations go directly from your code into the local db-file. So it is very simple, but works fine for this small application.
The database operations are implemented in a separate assembly, LocPoc.Repository.Sqlite.
As we have the implementation in a separate assembly and our API-code depends on the ILocationsRepositoryAsync interface, we can easily switch to a more advanced database later if needed.
This is the async version of the repository implementation with Sqlite:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using LocPoc.Contracts; | |
using Microsoft.EntityFrameworkCore; | |
using System; | |
using System.Collections.Generic; | |
using System.Threading.Tasks; | |
namespace LocPoc.Repository.Sqlite | |
{ | |
public class LocationsRepositoryAsync : ILocationsRepositoryAsync | |
{ | |
private readonly SqliteContext _context; | |
public LocationsRepositoryAsync(SqliteContext context) | |
{ | |
_context = context ?? throw new ArgumentNullException(nameof(context)); | |
} | |
public async Task<IEnumerable<Location>> GetAllAsync() | |
{ | |
return await _context.Locations.ToListAsync(); | |
} | |
public async Task<Location> GetAsync(string id) | |
{ | |
var location = await _context.Locations.FindAsync(id); | |
return location; | |
} | |
public async Task<Location> CreateAsync(Location location) | |
{ | |
location.Id = Guid.NewGuid().ToString(); | |
await _context.Locations.AddAsync(location); | |
await _context.SaveChangesAsync(); | |
return location; | |
} | |
public async Task<Location> UpdateAsync(Location location) | |
{ | |
var loc = await _context.Locations.FindAsync(location.Id); | |
if (loc != null) | |
{ | |
loc.Name = location.Name; | |
loc.Description = location.Description; | |
loc.Latitude = location.Latitude; | |
loc.Longitude = location.Longitude; | |
} | |
await _context.SaveChangesAsync(); | |
return loc; | |
} | |
public async Task DeleteAsync(string id) | |
{ | |
var loc = await _context.Locations.FindAsync(id); | |
if (loc != null) | |
{ | |
_context.Locations.Remove(loc); | |
} | |
await _context.SaveChangesAsync(); | |
} | |
} | |
} |
I use Entity Framework Core as an ORM (object-relational mapper) for working with the database. SqliteContext is an Entity Framework-DbContext subclass that handles Location entities with Sqlite:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.EntityFrameworkCore; | |
using LocPoc.Contracts; | |
namespace LocPoc.Repository.Sqlite | |
{ | |
public class SqliteContext: DbContext | |
{ | |
public SqliteContext(DbContextOptions<SqliteContext> options) : base(options) | |
{ | |
} | |
public DbSet<Location> Locations { get; set; } | |
protected override void OnConfiguring(DbContextOptionsBuilder options) | |
=> options.UseSqlite("Data Source=locpoc.db"); | |
} | |
} |
You can read about the details on how to get started with EF Core and Sqlite here: https://docs.microsoft.com/en-us/ef/core/get-started/?tabs=netcore-cli
Documenting the API
As an additional exercise I have integrated Swagger/OpenAPI with Swashbuckle.AspNetCore:
This creates an OpenAPI-spec of the API and generates a UI where you can browse the API and test it with requests:
Wrap-up
In this blog series I have shown how Angular and .NET Core can be used for making a geolocation application. This last post focused on how to make a ASP.NET Core WebAPI that provides services for managing a collection of Location items.
Future enhancements
The project can be extended with authentication and multi-user support. Other exercises can be new features like uploading photos for each location or creating location items from positioning data from uploaded photos.