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
applicationUrlPort, über welchen die Anwendung erreichbar ist - kann beliebig geändert werden, sofern dieser Port nicht schon benutzt ist oder nicht existiert
- enthält unter
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
- erstellt die Anwendung (bzw. bildet/kompiliert sie) und veröffentlicht sie ins Verzeichnis
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:
- in
docker-compose.ymlals Umgebungsvariableservices:
webapi:
environment:
DatabaseSettings__ConnectionString: mongodb://user:password@mongodb:27017 - in
docker-compose.ymlals Umgebungsvariable aus.env-Datei (noch nicht ausprobiert)MONGODB_URI=mongodb://user:password@mongodb:27017services:
webapi:
environment:
DatabaseSettings__ConnectionString: ${MONGODB_URI} - in
appsettings.jsonals 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:
- Postman
- eine der bekanntesten Anwendungen, um REST-APIs zu testen
- Installation auf Linux
snap install postman
- Insomnia
- 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"
]
}
- Requests können in einer
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-mongodbWebApi(C#-Projekt)binobjPropertieslaunchSettings.json
appsettings.Development.jsonappsettings.jsonDockerfileProgram.csWebApi.csproj
.gitignoredocker-compose.ymlREADME.md
Projekt-Code
Den gesamten Projekt-Code befindet sich auf GitHub: M165 M347 Minimal API with MongoDB