Zum Hauptinhalt springen

Microservice mit MongoDB und Docker

Microservices sind eine Architektur, die es ermöglicht, Anwendungen in kleine, unabhängige Dienste zu unterteilen. Diese Dienste können unabhängig voneinander entwickelt, bereitgestellt und skaliert werden. Docker ist eine beliebte Plattform zur Containerisierung von Anwendungen und eignet sich hervorragend für die Implementierung von Microservices.

Hinweis: Folgende Beispiele sind für Linux-Betriebssysteme (in meinem Fall Ubuntu) ausgelegt, da Docker-Container meistens auf Linux-Servern verwendet werden.

Beispiel: API mit MongoDB und Docker

1. Installation .NET 8

Überprüfung, ob .NET 8 bereits installiert ist:

dotnet --list-sdks

Falls .NET 8 nicht installiert ist, muss es zuerst heruntergeladen werden:

sudo apt-get update
sudo apt-get install -y dotnet-sdk-8.0

Mit dem oberen Befehl dotnet --list-sdks kann überprüft werden, ob die Installation erfolgreich war.

2. Erstellen des Projekts

Mit folgendem Befehl kann das Grundgerüst eines neuen Web-API-Projektes mit .NET 8 und C# erstellt werden:

dotnet new web --name WebApi --framework net8.0

Dieser Befehl verwendet die Vorlage web, um ein neues Projekt zu erstellen. Alle C#-, F#- und Visual Basic Vorlagen können mit dotnet new list aufgelistet werden.

Dies erstellt eine .gitignore-Datei, damit nur relevanter Quellcode des C#-Projektes ins Git-Repository eingecheckt wird:

dotnet new gitignore

3. Projekt starten

Das Projekt kann immer mit folgendem Befehl im Projekt-Ordner (hier WebApi) gestartet werden:

dotnet run

4. Projekt-Einstellungen

Properties/launchSettings.json:

  • profiles/http: http-Profil
    • enthält unter applicationUrl Port, über welchen die Anwendung erreichbar ist
    • kann beliebig geändert werden, sofern dieser Port nicht schon benutzt ist oder nicht existiert

5. Dockerfile erstellen

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS dotnet-build

WORKDIR /build

COPY ../../../../docs-cloud/docs/md/containerization/docker .

RUN ["dotnet", "restore"]
RUN ["dotnet", "publish", "-c", "Release", "-o", "out"]


FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS dotnet-runtime

WORKDIR /run

COPY --from=dotnet-build /build/out .

ENV ASPNETCORE_URLS=http://+:5001

EXPOSE 5001

# name of dll file is the same as the WebApi.csproj file name
ENTRYPOINT [ "dotnet", "WebApi.dll" ]

LABEL description="Dotnet WebApi"
LABEL author="Lorin Steiner"

Erklärungen:

  • dotnet restore
    • stellt alle Abhängigkeiten des Projektes her
  • dotnet publish -c Release -o out
    • erstellt die Anwendung (bzw. bildet/kompiliert sie) und veröffentlicht sie ins Verzeichnis out
  • ENV ASPNETCORE_URLS=http://+:5001
    • legt den Port fest, über den die Anwendung erreichbar ist
    • +
      • die ASP.NET Core-Anwendung soll auf allen verfügbaren Netzwerkadressen lauschen
      • steht für alle IP-Adressen
      • ähnlich wie 0.0.0.0

dotnet/sdk vs. dotnet/aspnet

  • dotnet/sdk
    • enthält alles, was Entwickler benötigen, um .NET-Anwendungen zu erstellen
    • um einiges grösser als dotnet/aspnet
  • dotnet/aspnet
    • enthält nur Laufzeitumgebung, um ASP.NET Core-Anwendungen auszuführen

6. docker-compose.yml erstellen

services:
webapi:
container_name: web-api
build: WebApi
ports:
- 5001:5001

7. API mit Docker starten

cd WebApi
docker compose up

8. API erweitern

Der Code der API ist in der Datei Program.cs zu finden:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

9. MongoDB-Container hinzufügen

Um MongoDB zu verwenden, muss der Container in die docker-compose.yml-Datei hinzugefügt werden:

services:
webapi:
container_name: web-api-with-mongo
build: WebApi
ports:
- 5001:5001

mongodb:
container_name: mongodb
image: mongo
volumes:
- mongodb-data:/data/db
environment:
- MONGO_INITDB_ROOT_USERNAME=gbs
- MONGO_INITDB_ROOT_PASSWORD=geheim
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh mongodb://gbs:geheim@mongodb:27017/admin --quiet
interval: 5s
timeout: 3s
retries: 5
start_period: 5s

volumes:
mongodb-data:

Bevor man den offiziellen .NET MongoDB.Driver benutzen kann, muss das entsprechende NuGet-Paket namens MongoDB.Driver installiert werden:

dotnet add package MongoDB.Driver

GET-Methode der URL /check in die C#-Datei Program.cs hinzufügen. Wenn man die URL aufruft, wird überprüft, ob die Verbindung zur MongoDB-Datenbank erfolgreich ist und es werden alle existierenden Datenbanken in einem Result.ok(...) zurückgegeben.

using MongoDB.Driver;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.MapGet("/", () => "Minimal API Version 1.0");

app.MapGet("/check", () => {
try {
string connectionUri = "mongodb://gbs:geheim@mongodb:27017";
var client = new MongoClient(connectionUri);

var databaseNames = client.ListDatabaseNames().ToList();

return Results.Ok(new {
Message = "db access ok",
Databases = databaseNames
});
} catch(Exception ex) {
return Results.Problem($"db access not ok: {ex.Message}");
}
});

app.Run();

10. MongoDB-Verbindung-URL als Umgebungsvariable speichern

Damit die MongoDB-Verbindung-URL nicht im Quellcode gespeichert werden muss, kann sie als Umgebungsvariable gespeichert werden. Dafür gibt es verschiedene Möglichkeiten:

  1. in docker-compose.yml als Umgebungsvariable
    services:
    webapi:
    environment:
    DatabaseSettings__ConnectionString: mongodb://user:password@mongodb:27017
  2. in docker-compose.yml als Umgebungsvariable aus .env-Datei (noch nicht ausprobiert)
     MONGODB_URI=mongodb://user:password@mongodb:27017
    services:
    webapi:
    environment:
    DatabaseSettings__ConnectionString: ${MONGODB_URI}
  3. in appsettings.json als Konfiguration
    {
    "ConnectionStrings": {
    "MongoDB": "mongodb://user:password@mongodb:27017"
    }
    }

Der Zugriff im C#-Code erfolgt dann so:

using MongoDB.Driver;

var builder = WebApplication.CreateBuilder(args);

// all environmental variables from the section "DatabaseSettings" are taken
var databaseSettingsSection = builder.Configuration.GetSection("DatabaseSettings");

// bind the environmental variables from the section to the properties of the class DatabaseSettings (definition see below)
builder.Services.Configure<DatabaseSettings>(databaseSettingsSection);

var app = builder.Build();

// the environmental variables are injected/passed as DatabaseSettings object in the delegate
app.MapGet("/check", (Microsoft.Extensions.Options.IOptions<DatabaseSettings> options) => {
try {
// the environmental variables object can be accessed through options.Value
var client = new MongoClient(options.Value.ConnectionString);

// get all database names
var databaseNames = client.ListDatabaseNames().ToList();

// return 200 OK with a message and the database names
return Results.Ok(new {
Message = "db access ok",
Databases = databaseNames
});
} catch(Exception ex) {
// return a 500 error
return Results.Problem($"db access not ok: {ex.Message}");
}
});

app.Run();
public class DatabaseSettings
{
public string ConnectionString { get; set; } = "";
}

11. Endpunkte mit CRUD-Operationen

Testen der API

Um eine Rest-API zu testen, gibt es unter anderem folgende Möglichkeiten:

  1. Postman
    • eine der bekanntesten Anwendungen, um REST-APIs zu testen
    • Installation auf Linux
      snap install postman
  2. Insomnia
  3. in Visual Studio Code mit der Erweiterung REST Client
    • Requests können in einer .http-Datei gespeichert werden:
      ### (divider)
      GET http://localhost:5001/check HTTP/1.1

      ###
      POST http://localhost:5001/api/values

      POST http://localhost:5001/api/movies
      Content-Type: application/json

      {
      "Title": "No Time To Die",
      "Year": 2000,
      "Summary": "This is a summary of the movie No TIme To Die!",
      "Actors": [
      "Actor 1",
      "Actor 2"
      ]
      }

Movie-Objekt

public class Movie
{
[BsonId]
public string Id { get; set; } = "";
public string Title { get; set; } = "";
public int Year { get; set; }
public string Summary { get; set; } = "";
public string[] Actors { get; set; } = Array.Empty<string>();
}

Das Attribut [BsonId] markiert das Property Id als Repräsentation des MongoDB _id-Attributs.

Service

Die Endpunkte eines Services sollte man zuerst mit einem Service-Interface definieren:

public interface IMovieService
{
public string Check();
void Create(Movie movie);
IEnumerable<Movie> Get();
Movie Get(string id);
void Update(string id, Movie movie);
void Remove(string id);
}

Von diesem Service kann man dann eine Implementierung erstellen:

Variante 1:

public class MongoMovieService : IMovieService
{
private readonly IMongoCollection<Movie> _movies;

public MovieService(IOptions<DatabaseSettings> databaseSettings)
{
var mongoClient = new MongoClient(databaseSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase("movieDb");
_movies = mongoDatabase.GetCollection<Movie>("movies");
}

public string Check()
{
return "ok";
}

public void Create(Movie movie)
{
_movies.InsertOne(movie);
}

public IEnumerable<Movie> Get()
{
return _movies.Find(_ => true).ToList();
}

public Movie Get(string id)
{
return _movies.Find(m => m.Id == id).FirstOrDefault();
}

public void Update(string id, Movie movie)
{
_movies.ReplaceOne(m => m.Id == id, movie);
}

public void Remove(string id)
{
_movies.DeleteOne(m => m.Id == id);
}
}

Variante 2:

public class MongoMovieService : IMovieService
{

private static readonly string DATABASE_NAME = "main";
private static readonly string COLLECTION_NAME = "movies";

private readonly MongoClient _client;
private readonly IMongoDatabase _mainDatabase;
private readonly IMongoCollection<Movie> _moviesCollection;

public MongoMovieService(IOptions<DatabaseSettings> options)
{
_client = new MongoClient(options.Value.ConnectionString);
_mainDatabase = _client.GetDatabase(DATABASE_NAME);
_moviesCollection = _mainDatabase.GetCollection<Movie>(COLLECTION_NAME);
}

public string Check()
{
try
{
var databaseNames = _client.ListDatabaseNames().ToList();

return $"db access ok. Databases=[{String.Join(",", databaseNames)}]";
}
catch (Exception ex)
{
return $"db access not ok: {ex.Message}";
}
}

public void CreateMovie(Movie movie)
{
if (GetMovie(movie.Id) == null)
{
_moviesCollection.InsertOne(movie);
}
}

public bool DeleteMovie(string id)
{
DeleteResult deleteResult = _moviesCollection.DeleteOne(Builders<Movie>.Filter.Eq(m => m.Id, id));
return deleteResult.DeletedCount == 1;
}

public Movie? GetMovie(string id)
{
return _moviesCollection.Find(Builders<Movie>.Filter.Eq(m => m.Id, id)).FirstOrDefault();
}

public IEnumerable<Movie> GetMovies()
{
return _moviesCollection.Find(_ => true).ToList();
}

public bool UpdateMovie(string id, Movie movie)
{
ReplaceOneResult replaceOneResult = _moviesCollection.ReplaceOne(m => m.Id == id, movie);
return replaceOneResult.ModifiedCount == 1;
}
}

Um den Service in der API zu verwenden, muss er zuerst in der Program.cs-Datei registriert werden:

builder.Services.AddSingleton<IMovieService, MongoMovieService>();

Somit kann er in allen API Map-Methoden als Parameter angegeben werden. Das Framework injected dann automatisch ein Singleton-Objekt des Services vom Typ MongoMovieService (Dependency Injection).

app.MapGet("/check", (IMovieService movieService) => {
return movieService.Check();
});

Debuggen

Um in Visual Studio Code diese API zu debuggen, muss der MongoDB-Container zuerst einzeln gestartet werden. Dies kann über den Befehl docker compose up mongodb oder in der docker-compose.yml-Datei in Visual Studio Code über die Beschriftung Run Service erfolgen.

Damit die API auf den MongoDB-Server-Container zugreifen kann, muss der Container-Namen (ConnectionString) in der docker-compose.yml-Datei zur IP-Adresse des MongoDB-Containers geändert werden. Die IP-Adresse des Containers kann über Portainer oder über den Befehl herausgefunden werden. Danach kann unter Run and Debug (Ctrl+Shift+D) die Konfiguration C#: WebApi ausgewählt und gestartet werden.

Projekt-Struktur

  • api-with-mongodb
    • WebApi (C#-Projekt)
      • bin
      • obj
      • Properties
        • launchSettings.json
      • appsettings.Development.json
      • appsettings.json
      • Dockerfile
      • Program.cs
      • WebApi.csproj
    • .gitignore
    • docker-compose.yml
    • README.md

Projekt-Code

Den gesamten Projekt-Code befindet sich auf GitHub: M165 M347 Minimal API with MongoDB