Backend mit Azure Cloud

In unserem ersten Beitrag dieser Reihe werden wir ein Backend zum Speichern und Abfragen der Daten anlegen. Wie der Titel schon verrät wird dieses in der Azure-Cloud hostet.

Die von Microsoft entwickelte Cloud-Pattform Azure ist neben Amazon und Google einer der Großen und bietet zahlreiche verschiedene Services an. Der große Vorteil der Cloud liegt vor Allem darin, dass die Service schnell eingerichtet und danach direkt benutzbar sind. Die Kosten sind dabei überschaubar, da häufig nur für die aktive Nutzungszeit eines Service bezahlt werden muss.

Architekturübersicht

Für unser Backend, welcher letztendlich nur ein REST-Service mit einer „Datenbank“ ist, verwenden wir von Azure folgende Services:

  • App-Service: Hostet den in C# implementierten ASP.NET REST-Service.
  • Cosmos-DB: Speichert die strukturierten Daten als JSON.
  • BLOB-Storage: Speichert alle Binärdaten wie z.B. die Anhänge.
Systemarchitektur des Backends in der Azure Cloud

Aufsetzen der Cosmos-DB und des BLOB-Storage

Die Cosmos-DB ist Microsofts Variante einer NoSQL-Datenbank. Sie speichert dabei die Daten als JSON. Im Gegensatz zu anderen NoSQL-Datenbanken bietet Azure als API für die Cosmos-DB verschiedene Standards für die Abfrage an. Neben dem klassiger MongoDB auch SQL oder Cassandra.

Die Cosmos-DB lässt sich schnell und einfach im Azure Portal erstellen. Dabei ist vor allem die Einstellungen der Firewall entscheidend. Da die Cosmos-DB nur vom REST-Service erreichbar sein soll, erlauben wir nur den Zugriff von bestimmten IP-Adressen. 

Der Azure BLOB-Storage dient zum Speichern von Dateien jeglicher Art. Microsoft versteht darunter unter anderem Binärdateien, Bilder, Videos, Audio und Text.

Die Anlage erfolgt genauso einfach wie die Comsos-DB über das Azure-Portal. Für den Moment belassen wir den Zugriff auf den BLOB-Storage bei Public, da wir sonst im weiteren Verlauf Probleme mit der Firewall bekommen, wenn der im Azure App Service gehostete REST-Service auf den BLOB-Storage zugreifen möchte. Später werden wir den Zugriff noch auf Privat ändern und nur bestimmte Services den Zugriff erlauben.

Implementierung des REST-Service

Für die Implementierung des REST-Service nehmen wir aufgrund der Nähe zu Microsoft ASP.NET mit .NET Core 3.1. Hier implementieren wir einen einfachen REST-Service. Die angebotene Schnittstelle beinhaltet dabei folgende Endpunkte:

Bei der Implementierung legen wir die zwei Controller TaskController und SearchController an. Die Ausgliederung der Suche machen wir, sodass der Service später um weitere Objekte erweitert werden kann, welche ebenfalls durchsucht werden können.

Wichtig bei einer Klassenhierarchie ist korrekte Kapseln der Business-Logik. Aus diesem Grund beinhalten die Controller nur die Logik zum Ermitteln der Daten aus der Anfrage und das erstellen der Antwort. Der Zugriff auf die anderen Dienste (Cosmos-DB und BLOB-Storage) werden über den AzureCloudService gekapselt, welcher den Controller über das Interface ICloudService bereitgestellt wird. Anschließend haben wir noch je einen Service für die Cosmos-DB und den BLOB-Storage.

Klassenhierachie des implementierten REST-Service

Im Folgenden ist die vollständige Implementierung der HTTP-Methode für die Anlage eines ToDo-Eintrages in den einzelnen betroffenen Klassen dargestellt.

using System;
using Microsoft.AspNetCore.Mvc;
using RestService.Model;
using RestService.Services;

namespace RestService.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TaskController : ControllerBase
    {
        private readonly ICloudService _cloudService;

        public TaskController(ICloudService cloudService)
        {
            _cloudService = cloudService;
        }

        // POST: api/Task
        [HttpPost]
        public async Task<ActionResult> Post([FromBody] TaskItem item)
        {
            item.Id = Guid.NewGuid().ToString();
            await _cloudService.AddTaskAsync(item).ConfigureAwait(false);

            return Ok(item);
        }
    }
}
using System.Threading.Tasks;
using RestService.Model;

namespace RestService.Services
{
    public interface ICloudService
    {
        public Task AddTaskAsync(TaskItem item);
    }
}
using System.Threading.Tasks;
using RestService.Model;

namespace RestService.Services
{
    public class AzureCloudService : ICloudService
    {
        private readonly CosmosDbService _cosmosDb;

        public AzureCloudService(CosmosDbService cosmosDbService)
        {
            _cosmosDb = cosmosDbService;
        }

        public async Task AddTaskAsync(TaskItem item)
        {
            await _cosmosDb.AddTaskAsync(item).ConfigureAwait(false);
        }
    }
}
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Fluent;
using Microsoft.Extensions.Configuration;
using Nito.AsyncEx;
using RestService.Model;

namespace RestService.Services
{
    public class CosmosDbService
    {
        private static IConfigurationSection _cosmosDbConfig;

        private readonly AsyncLazy<Container> _container = new AsyncLazy<Container>(async () =>
        {
            var databaseName = _cosmosDbConfig.GetSection("DatabaseName").Value;
            var containerName = _cosmosDbConfig.GetSection("ContainerName").Value;
            
            //...

            var container = client.GetContainer(databaseName, containerName);
            return container;
        });

        public CosmosDbService(IConfiguration configuration)
        {
            _cosmosDbConfig = configuration.GetSection("CosmosDb");
        }

        public async Task AddTaskAsync(TaskItem item)
        {
            await (await _container.ConfigureAwait(false)).
                CreateItemAsync(item, new PartitionKey(item.Id));
        }
    }
}
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;

namespace RestService.Model
{
    public class TaskItem
    {
        [Required]
        [JsonProperty(PropertyName = "id")]
        public string Id { get; set; }

        [Required]
        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }

        [Required]
        [JsonProperty(PropertyName = "priority")]
        public int Priority { get; set; }

        [JsonProperty(PropertyName = "description")]
        public string Description { get; set; }

        [JsonProperty(PropertyName = "attachments")]
        public List Attachments { get; set; }
        
    }
}

Deployment der Komponenten

Für das Deployment des REST-Service können wir Visual Studio verwenden. Es bietet uns direkt die Möglichkeit das Projekt auf ein neuen oder bestehenden App Service zu deployen.

Nachdem dies erledigt ist, müssen wir nun nochmal zurück zu unsere Firewall. Dazu können wir beim App Service für den REST-Service unter Einstellungen > Eigenschaften die möglichen IP-Adressen unseres REST-Service sehen, welche wir anschließend bei der Cosmos-DB in der Firewall freischalten müssen. 

Wenn wir nun einen REST-Endpunkt mit URL vom App Service aufrufen, erhalten wir die entsprechende Antwort mit Daten.