Migration ist nicht alles: Monitoring und Telemetrie

Moderne Entwicklungs-Stacks wie .NET Core bieten mittlerweile zahlreiche Funktionen zur Unterstützung von Monitoring, Telemetrie und flexibler Konfiguration, Dies sollte bei der Migration einer Legacy-Anwendung unbedingt mit berücksichtigt werden.

Kategorie: Migration · 10/24/2024

Einleitung

Nachdem sich die ersten Artikel dieser Serie hauptsächlich mit der Vorgehensweise zur Migration von Softwareanwendungen beschäftigt haben, soll es heute um eine Reihe von Zusatzaspekten gehen, die vor allem durch die Veränderungen in der IT-Landschaft der letzten 10 bis 15 Jahre bedingt sind. Früher wurden Kernanwendungen dediziert auf eigenen Servern gehostet – häufig unter Windows (Server) –, heute spielen hingegen das Deployment in Cloud-Umgebungen oder On-Premise-Kubernetes-Umgebungen eine wesentlich größere Rolle. Damit einhergehend gewinnen Themen wie:
 
  • nahtloses Deployment (Stichwort: DevOps bzw. DevSecOps),
  • (horizontale) Skalierung,
  • Telemetrie/Überwachung,
immer mehr an Bedeutung. Moderne Entwicklungs-Stacks wie .NET Core (siehe vorheriger Beitrag) bieten mittlerweile zahlreiche Funktionen zur Unterstützung dieser Prozesse und Anforderungen, die in älteren Versionen noch nicht vorhanden waren. Daher kann die Migration einer Legacy-Anwendung niemals eine einfache 1:1-Umsetzung auf aktuelle SDKs und Technologien sein; vielmehr müssen auch diese immer wichtigeren und aktuellen Anforderungen berücksichtigt werden.
 
Im Folgenden möchte ich einige Erfahrungen und Lösungsansätze vorstellen, die wir im Laufe des letzten Jahres in verschiedenen Migrationsprojekten gesammelt haben. Diese umfassten unter anderem:
 
  • die Neuerstellung eines Controller-basierten ASP.NET-Systems mit Frontend und Backend für eine Verwertungsgesellschaft, mit Kubernetes und ASP.NET Core als Zielumgebung,
  • die Modernisierung einer Remote-Control-Website inklusive der zugehörigen Dienste und VPN-Infrastruktur zur Steuerung entfernter Geräte per API und VNC, ebenfalls migriert auf die .NET (Core)-Plattform,
  • die Migration einer Xamarin-Forms-App zur Gerätesteuerung (über UDP Broadcast und HTTP REST API) zu .NET MAUI mit Unterstützung der Plattformen iOS, Android und Windows UWP.
 
Die Reihenfolge der vorgestellten Lösungsansätze ist willkürlich und erhebt keinen Anspruch auf Vollständigkeit. Die Bedeutung für einzelne Anwendung hängt stark von der Art der Anwendung ab.
 
 

Ablösung von dateibasiertem Logging durch OpenTelemetry

In der modernen Softwareentwicklung ist effektives Logging unerlässlich, um Anwendungen zu überwachen, Fehler zu diagnostizieren und die Leistung zu optimieren. Traditionell haben viele Anwendungen auf dateibasiertes Logging gesetzt, bei dem Logdaten lokal auf dem Server gespeichert werden. Mit der zunehmenden Komplexität verteilter Systeme und dem Übergang zu cloudbasierten Architekturen stößt dieses Modell jedoch an seine Grenzen.
OpenTelemetry hat sich hier als standardisierte, plattformunabhängige Lösung etabliert, die das Sammeln von Metriken, Logs und Traces aus verschiedenen Quellen ermöglicht. Die einfache Integration von OpenTelemetry in ASP.NET Core Anwendungen bietet eine Reihe von Vorteilen sowohl gegenüber dem traditionellen dateibasierten Logging als auch gegenüber proprietären Logging Lösungen:
 
  1. Zentralisierte Überwachung: OpenTelemetry ermöglicht es, Logdaten aus verschiedenen Diensten und Komponenten an einem zentralen Ort zu sammeln und zu analysieren. Dies erleichtert die Überwachung und Fehlersuche in komplexen, verteilten Systemen.
 
  1. Standardisierung: Als Open-Source-Projekt bietet OpenTelemetry einen einheitlichen Standard für Telemetriedaten. Dies fördert die Interoperabilität zwischen verschiedenen Tools und Plattformen und verhindert Vendor-Lock-in.
 
  1. Skalierbarkeit: OpenTelemetry ist für den Einsatz in groß angelegten, skalierbaren Anwendungen konzipiert. Es kann (abhängig vom Backend) große Mengen an Telemetriedaten effizient verarbeiten, ohne die Anwendungsleistung zu beeinträchtigen.
 
  1. Verbesserte Einblicke: Durch die Unterstützung von verteiltem Tracing können Entwickler die Ausführung von Anfragen über verschiedene Dienste hinweg nachverfolgen. Dies bietet tiefere Einblicke in das Systemverhalten und hilft bei der schnellen Identifizierung von Engpässen.
 
  1. Cloud-Native Unterstützung: OpenTelemetry ist speziell für cloudbasierte Umgebungen entwickelt worden und integriert sich nahtlos mit modernen Cloud-Technologien und -Diensten.
 
  1. Reduzierter Wartungsaufwand: Da OpenTelemetry als Managed Service oder in Form von leicht zu aktualisierenden Bibliotheken bereitgestellt wird, reduziert sich der Aufwand für Wartung und Aktualisierung im Vergleich zu individuellen Logging-Lösungen.
 
Für den Aufbau eines leistungsfähigen Backends gibt es verschiedene Lösungen:
 

Ersatz der "klassischen" Konfiguration per ConfigurationManager durch die IConfiguration Schnittstelle

Der Übergang vom traditionellen `.NET ConfigurationManager` zur `IConfiguration` Schnittstelle in ASP.NET Core erwies sich als vorteilhaft, besonders in unserer Rancher RKE2 K8S Umgebung. Die Gründe dafür lassen sich wie folgt zusammenfassen:
 
  1. Flexibilität bei Konfigurationsquellen: `IConfiguration` unterstützt eine Vielzahl von Konfigurationsquellen wie JSON-, XML- und INI-Dateien, Umgebungsvariablen, Befehlszeilenargumente und benutzerdefinierte Quellen. Dies ermöglicht es, Konfigurationen dynamisch und flexibel zu gestalten, ohne den Code ändern zu müssen.
 
  1. Einfache Integration von Umgebungsvariablen: In Cloud-Umgebungen ist es üblich (und sinnvoll), Konfigurationen über Umgebungsvariablen zu steuern. `IConfiguration` integriert diese nahtlos, was die Anpassung der Anwendung an verschiedene Umgebungen erleichtert.
 
  1. Unterstützung von Abhängigkeitsinjektion: `IConfiguration` ist vollständig in das Dependency Injection-System von ASP.NET Core integriert. Dies fördert lose Kopplung und erleichtert das Testen und die Wartung der Anwendung.
 
  1. Hierarchische Konfigurationen: Mit `IConfiguration` können Konfigurationswerte hierarchisch strukturiert werden. Dies ermöglicht eine übersichtlichere Organisation komplexer Konfigurationsdaten.
 
  1. Laufzeitaktualisierung: Bestimmte Konfigurationsquellen können so eingerichtet werden, dass sie zur Laufzeit aktualisiert werden. Dies ist besonders nützlich in Umgebungen, in denen Downtime minimiert werden muss.
 
  1. Cloud-Native Ausrichtung: `IConfiguration` ist für moderne, Cloud-native Anwendungen konzipiert und unterstützt die Prinzipien der 12-Factor App Methodik, was die Skalierbarkeit und Portabilität der Anwendung verbessert.
Vorteile der Konfiguration per Umgebungsvariablen in Cloud-basierten Umgebungen:
 
  • Umgebungsabhängige Konfiguration: Umgebungsvariablen ermöglichen es, Konfigurationen einfach zwischen Entwicklungs-, Test- und Produktionsumgebungen zu variieren, ohne den Anwendungscode zu ändern.
 
  • Sicherheit: Sensible Daten wie Verbindungszeichenfolgen oder API-Schlüssel können sicher über Umgebungsvariablen verwaltet werden, anstatt sie in Konfigurationsdateien oder im Code zu hinterlegen.
 
  • Einfache Skalierung und Bereitstellung: In Cloud-Umgebungen und Container-Orchestrierungssystemen wie Kubernetes oder Docker Swarm können Umgebungsvariablen zentral verwaltet und automatisch an neue Instanzen verteilt werden.
 
  • Reduzierung von Konfigurationsfehlern: Da Umgebungsvariablen außerhalb der Anwendung verwaltet werden, verringert sich das Risiko von Konfigurationsfehlern beim Deployment.
 
  • Kontinuierliche Integration und Deployment (CI/CD): Umgebungsvariablen lassen sich leicht in CI/CD-Pipelines integrieren, was automatisierte Deployments in verschiedene Umgebungen erleichtert.

ASP.NET HealthChecks: Verwendung und Vorteile

ASP.NET HealthChecks sind ein weiteres nützliches Feature, das zusätzlich in bereits migrierten ASP.NET Anwendungen mit integriert werden sollte um einfach die Verfügbarkeit der jeweiligen Anwendung zu überwachen. Sie ermöglichen es Entwicklern und Systemadministratoren, sicherzustellen, dass Dienste und Ressourcen, auf die sich ihre Anwendung stützt, ordnungsgemäß funktionieren. Und zwar sowohl auf allgemeinem Level (Speicherplatz, TLS Zertifikatsgültigkeit etc.) als auch mit sehr spezifischen, auf die Anwendung zugeschnittenen Prüfungen.
 
1. Was sind ASP.NET HealthChecks?
 
ASP.NET HealthChecks sind teils vordefinierte, teils selbst (als C# Klasse) implementierte Mechanismen, um den Zustand verschiedener Teile einer Anwendung zu überprüfen, wie etwa Datenbanken, APIs, Caches oder andere externe Dienste. Sie geben Auskunft darüber, ob eine bestimmte benötigte Ressource verfügbar und funktionsfähig ist. Diese Checks sind natürlich besonders wichtig in verteilten Systemen und Microservice-Architekturen mit vielen Abhängigkeiten.
 
Die HealthChecks werden in ASP.NET Core über ein Middleware registriert und können über einen Endpunkt (typischerweise `/health`) abgerufen werden. Sie liefern standardisierte Informationen, die von Monitoring- und Alerting-Tools weiterverarbeitet werden können, z.B. für Liveness und Readiness Probes in Kubernetes. Dabei ist sowohl eine aggregierte Ausgabe als einfaches Wort (Healthy, Degraded, Unhealthy) als auch eine detailliertere JSON Ausgabe möglich.
 
2. Vorteile von ASP.NET HealthChecks
 
  • Frühe Erkennung von Problemen: HealthChecks ermöglichen eine frühzeitige Erkennung von Problemen, bevor diese das Endnutzer-Erlebnis beeinträchtigen. Das Monitoring kann regelmäßig überprüfen, ob alle Komponenten der Anwendung einwandfrei funktionieren.
 
  • Einfache Integration: ASP.NET HealthChecks sind leicht - auch nachträglich - in bestehende Anwendungen zu integrieren, da sie Teil des ASP.NET Core Frameworks sind und über einfache Konfigurationen gesteuert werden können.
 
  • Automatisiertes Monitoring und Alerting: Die Ergebnisse von HealthChecks können in Monitoring-Systeme wie Prometheus, Grafana oder Azure Application Insights integriert werden. Dies ermöglicht es, bei Problemen sofortige Benachrichtigungen zu senden.
 
  • Anpassbarkeit und Erweiterbarkeit: ASP.NET Core bietet eine Vielzahl an vordefinierten HealthChecks, und Entwickler können eigene Checks implementieren, die spezifisch auf die Bedürfnisse ihrer Anwendung zugeschnitten sind.
 
3. Vordefinierte HealthChecks in ASP.NET Core
 
ASP.NET Core bietet eine Reihe von vordefinierten HealthChecks für gängige Anwendungsfälle. Hier sind einige Beispiele:
 
  • SQL Server HealthCheck: Prüft die Erreichbarkeit und Funktionalität eines SQL Server Datenbankservers, indem eine einfache Abfrage ausgeführt wird. Dies ist nützlich, um sicherzustellen, dass die Verbindung zur Datenbank ordnungsgemäß funktioniert.
 
  • Redis HealthCheck: Überprüft die Erreichbarkeit eines Redis-Caches. Dies ist wichtig, wenn Ihre Anwendung Redis als Speicher oder Cache verwendet, um sicherzustellen, dass Daten schnell und zuverlässig abgerufen werden können.
 
  • Uri HealthCheck: Prüft, ob eine externe API oder ein Webdienst erreichbar ist. Dieser HealthCheck kann verwendet werden, um sicherzustellen, dass alle externen Abhängigkeiten Ihrer Anwendung verfügbar sind.
 
  • Disk Storage HealthCheck: Überwacht den Speicherplatz auf einem bestimmten Laufwerk, um sicherzustellen, dass Ihre Anwendung genügend Speicherplatz hat, um ordnungsgemäß zu funktionieren.
 
  • Memory HealthCheck: Überprüft, ob Ihre Anwendung innerhalb der konfigurierten Grenzen für den Speicherverbrauch bleibt, um Out-Of-Memory-Fehler zu vermeiden.
4. Implementierung von eigenen HealthChecks
 
Um HealthChecks in ASP.NET Core zu verwenden, müssen diese in der `Program.cs`-Datei konfiguriert werden:
services.AddHealthChecks()
    .AddSqlServer(Configuration.GetConnectionString("DefaultConnection"))
    .AddRedis(Configuration["RedisConnectionString"])
    .AddCheck<CustomHealthCheck>("custom_check");

:

 app.UseEndpoints(endpoints =>
 {
     endpoints.MapHealthChecks("/health");
 });
Hier wird ein SQL Server- und ein Redis-HealthCheck hinzugefügt. Zusätzlich wird ein benutzerdefinierter HealthCheck registriert, der weitere Überprüfungen durchführen kann.
 
5. HealthChecks UI: Die Benutzeroberfläche zur Visualisierung
 
Die ASP.NET Core HealthChecks UI ist eine Erweiterung, die eine benutzerfreundliche Oberfläche bietet, um den Status Ihrer HealthChecks in Echtzeit zu überwachen. Sie zeigt eine Liste aller registrierten HealthChecks sowie deren Ergebnisse an und ermöglicht es, auf einen Blick zu sehen, ob und wo Probleme bestehen.
 
Installation der HealthChecks UI:
 
Um die HealthChecks UI zu verwenden, müssen die entsprechenden Pakete zum Projekt hinzugefügt werden:

dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
Dann kann die UI in der `Program.cs`-Datei konfiguriert werden:
services.AddHealthChecksUI()
    .AddInMemoryStorage();

:

 app.UseEndpoints(endpoints =>
 {
        endpoints.MapHealthChecksUI();
 });
Die UI wird unter `/healthchecks-ui` bereitgestellt und bietet eine visuelle Darstellung der aktuellen Zustände der verschiedenen Checks.

Einsatz eines Übersichts-Dashboards

Anwendungen, die mit Hilfe eines modernen Techstacks migriert wurden, sind meist nicht mehr monolithisch aufgebaut, sondern bestehen aus verschiedenen Anwendungsbestandteilen bzw. Services. Pro Bestandteil werden im Allgemeinen verschiedene Endpunkte bereitgestellt, nicht nur fachliche Endpunkte sondern z.B. auch die oben erwähnten HealthCheck Endpunkte sowie oft auch weitere administrative Endpunkte, wie z.B. statistische Informationen und Telemetriedaten. 
Um Administratoren bzw. DevOps hier einen schnellen Überblick zu verschaffen, ist unserer Erfahrung nach ein anwendungsbezogenes Dashboard hilfreich, welches auf einen Blick den Status der verschiedenen Anwendungsbestandteile zeigt und die administrativen Links pro Bestandteil zeigt und aufrufbar macht. Zwar lässt sich diese Aussage auch über Tools wie ArgoCD oder Grafana gewinnen, jedoch sind hier oft viel zu viele Infos für einen simplen Überblick enthalten. 
Deswegen haben wir hier eine einfache, eigene flexibel konfigurierbare Dashboard-Anwendung erstellt.
 
 

Überarbeitung von APIs, ggf. Ersatz controllerbasierter APIs durch MinimalAPIs oder FastEndpoints

Mehr in den Bereich Refactoring fällt der Ansatz, controllerbasierte APIs in ASP.NET durch Minimal APIs zu ersetzen. Trotz des höheren Initialaufwandes einer Migration sind die Vorteile überlegenswert:
 
  1. Weniger Boilerplate-Code: Minimal APIs reduzieren den notwendigen Code erheblich. Entwickler können Endpunkte direkt in der `Program.cs` definieren, ohne separate Controller-Klassen und umfangreiche Routing-Konfigurationen erstellen zu müssen.
 
  1. Verbesserte Performance: Durch den geringeren Overhead und die vereinfachte Middleware-Pipeline bieten Minimal APIs oft eine bessere Performance im Vergleich zu traditionellen MVC-Controllern. Minimal APIs werden auch von der nativen AOT Kompilierung unterstützt (im Gegensatz zum MVC Ansatz) - die zu einem wesentlichen Performancegewinn führen kann.
 
  1. Einfachheit und Schnellere Entwicklung: Sie ermöglichen einen schnelleren Start bei der Entwicklung kleinerer Dienste oder Microservices, da weniger Setup und Konfiguration erforderlich sind.
 
  1. Direkte Kontrolle über Routing und Middleware: Entwickler haben mehr Flexibilität bei der Definition von Routen und können Middleware gezielter einsetzen.
 
  1. Schnelleres Anwendungsstartverhalten: Weniger initialisierte Komponenten führen zu kürzeren Startzeiten der Anwendung.
Ein weiterer alternativer Ansatz ist die Verwendung der Bibliothek FastEndpoints mit Vorteilen im Vergleich zu MVC-Controllern und Minimal APIs:
 
  1. Strukturierte Organisation: FastEndpoints bietet eine klare Trennung zwischen Anfrage, Verarbeitung und Antwort durch die Verwendung von dedizierten Endpoint-Klassen. Dies fördert sauberen und wartbaren Code.
 
  1. Erweiterte Funktionalität ohne Overhead: Die Bibliothek fügt zusätzliche Features hinzu, wie z. B. automatisierte Validierung, ohne den Overhead von MVC-Controllern zu verursachen.
 
  1. Verbesserte Performance: FastEndpoints ist für hohe Leistung optimiert und kann schneller sein als sowohl MVC-Controller als auch Minimal APIs, da es auf unnötige Abstraktionen verzichtet.
 
  1. Integrierte Validierung und Filter: Eingebaute Unterstützung für Anfragevalidierung, Authentifizierung und Autorisierung erleichtert die Entwicklung sicherer APIs.
 
  1. Konsistente Entwicklungserfahrung: Durch die Verwendung von Endpoint-Klassen fördert FastEndpoints konsistente Coding-Standards innerhalb eines Teams oder Projekts.
 
  1. Flexibilität und Anpassbarkeit: Entwickler können die Pipeline leicht erweitern und anpassen, was bei MVC-Controllern oft komplizierter ist.
 
  1. Bessere Unterstützung für API-Versionierung und Dokumentation: FastEndpoints erleichtert die Implementierung von API-Versionierung und die Integration von Tools wie Swagger für die API-Dokumentation.
 
Generell empfehlen wir die Migration auf FastEndpoints oder MinimalAPIs aber nur, wenn eine konsistente Umsetzung innerhalb aller Teilprojekte gewährleistet werden kann. Dies erspart die gedanklichen "Umschaltzeiten" bei Entwicklern, die mehrere (Teil-)Projekte betreuen.

Verwendung von Problemdetails nach RFC-9457

Die Verwendung von ProblemDetails (nicht nur) in ASP.NET Core erleichtert die Fehlerbehandlung und -darstellung in Web-APIs. ProblemDetails ist ein standardisiertes Format gemäß RFC9457 (die RFC7807 ersetzt) für Fehlerantworten, das konsistente und maschinenlesbare Informationen über aufgetretene Probleme bereitstellt.
 
  1. Standardisierte Fehlerantworten: Durch die Implementierung von ProblemDetails erhalten Clients Fehlerinformationen in einem einheitlichen Format, was die Verarbeitung und Anzeige von Fehlern vereinfacht.
 
  1. Konsistente und detaillierte Fehlerinformationen: ProblemDetails ermöglicht es, zusätzliche Details wie `type`, `title`, `status`, `detail` und `instance` in Fehlerantworten einzuschließen. Dies erleichtert das Debugging und die Fehlerbehebung.
 
  1. Verbesserte Sicherheit: Mit `app.UseExceptionHandler()` können ggf. Ausnahmen zentral abgefangen und behandelt werden, ohne dabei sensible Informationen an den Client preiszugeben.
 
  1. Anpassbare Fehlerbehandlung: Durch die Registrierung von `builder.Services.AddProblemDetails()` können Entwickler die Darstellung von Fehlern anpassen, z. B. durch Hinzufügen benutzerdefinierter Fehlercodes oder zusätzlicher Metadaten.
 
  1. Unterstützung für verschiedene HTTP-Statuscodes: `app.UseStatusCodePages()` stellt sicher, dass auch HTTP-Statuscodes ohne Inhalt (wie 404 oder 500) eine informative Antwort im ProblemDetails-Format liefern.
 
Details zur Konfiguration:
 
  1. builder.Services.AddProblemDetails():
 
  • Registrierung der Dienste: Diese Methode fügt die erforderlichen Dienste zur Verwendung von ProblemDetails hinzu.
  • Konfigurationsmöglichkeiten: Entwickler können Optionen festlegen, wie z. B. das Einbinden von Ausnahmeinformationen oder das Anpassen von Fehlerantworten je nach Umgebung (Entwicklung, Staging, Produktion).
  • Beispiel:
 builder.Services.AddProblemDetails(options =>
 {
     options.IncludeExceptionDetails = (context, exception) =>
     {
         // Ausnahmeinformationen nur in der Entwicklungsumgebung einbinden
         var env = context.RequestServices.GetRequiredService<IHostEnvironment>();
         return env.IsDevelopment();
     };
 });
  1. app.UseExceptionHandler():
 
  • Globale Ausnahmebehandlung: Diese Middleware fängt alle unbehandelten Ausnahmen ab, die in der Anwendung auftreten.
  • Integration mit ProblemDetails: In Kombination mit ProblemDetails können detaillierte Fehlerinformationen bereitgestellt werden, ohne interne Implementierungsdetails preiszugeben.
  • Sicherheitsaspekt: Verhindert die Weitergabe von Stack-Traces oder sensiblen Informationen an den Client.
  1. app.UseStatusCodePages():
 
  • Behandlung von Statuscodes ohne Inhalt: Diese Middleware generiert Antworten für HTTP-Statuscodes, die normalerweise keinen Inhalt haben.
  • Konsistente Fehlerdarstellung: Stellt sicher, dass auch für Fehler wie 404 (Nicht gefunden) oder 401 (Nicht autorisiert) informative Antworten im ProblemDetails-Format gesendet werden.
  • Beispielkonfiguration:
app.UseStatusCodePages(context =>
 {
     var problemDetailsFactory = context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
     var problemDetails = problemDetailsFactory.CreateProblemDetails(context.HttpContext, context.HttpContext.Response.StatusCode);
     return context.HttpContext.Response.WriteAsJsonAsync(problemDetails);
 });
Zusammenfassung der Vorteile:
 
  • Verbesserte Entwicklerproduktivität: Durch die zentrale Fehlerbehandlung und das standardisierte Format müssen Entwickler weniger Zeit auf die Implementierung individueller Fehlerlösungen verwenden.
  • Bessere Nutzererfahrung: Clients erhalten klare und konsistente Fehlermeldungen, was die Interaktion mit der API erleichtert.
  • Einfacheres Monitoring und Logging: Einheitliche Fehlerantworten erleichtern das Monitoring und die Analyse von Problemen in Produktionsumgebungen.
  • Flexibilität und Erweiterbarkeit: Die Konfiguration kann an die spezifischen Bedürfnisse der Anwendung angepasst werden, z. B. durch Hinzufügen von benutzerdefinierten Eigenschaften zu ProblemDetails.
 
Komplette Beispielimplementierung:
builder.Services.AddProblemDetails(options =>
{
    options.IncludeExceptionDetails = (context, exception) =>
    {
        // Ausnahmeinformationen nur in der Entwicklungsumgebung einschließen
        var env = context.RequestServices.GetRequiredService<IHostEnvironment>();
        return env.IsDevelopment();
    };
});

// Weitere Service-Registrierungen
builder.Services.AddControllers();

var app = builder.Build();

// Verwendung von ExceptionHandler und StatusCodePages
app.UseExceptionHandler();
app.UseStatusCodePages();

app.UseHttpsRedirection();
app.UseAuthorization();

app.MapControllers();

app.Run();

Ersatz "schwergewichtiger" DB Server

Ein weiterer Aspekt bei einer Migration in eine containerbasierte Umgebung ist der möglichst sparsame Umgang mit Ressourcen. In .NET Anwendungen, die über uns im letzten Jahr nach .NET (Core) migriert wurden bildete die Datenbasis oft der MS-SQL Server, der dediziert in einer eigenen Hardware/bzw. VM gehostet wurde. Aufgrund des doch recht hohen Ressourcenbedarfs sollte hier geprüft werden, ob ein Ersatz durch eine "leichtgewichtigere" Datenbank wie PostgreSQL oder MySQL bzw. MariaDB möglich ist. Oft ist die "Power" des MS-SQL Servers mit seinen zahllosen Features nicht wirklich für jede Anwendung notwendig. Unserer Erfahrung nach lassen sich kleine bis mittlere Datenbankmodelle mit überschaubarem Aufwand migrieren, gerade wenn auch ein ORM Layer wie Entity Framework zum Einsatz kommt. Ergebnis ist dann
 
  • Weniger RAM Bedarf (MS-SQL 1..4GB, MariaDB 512MByte, PostgreSQL 1GB)
  • Weniger Speicherplatz für DB Server (MS-SQL ca. 3GB, MariaDB 200.660MByte, PostgreSQL ca. 300MByte)
  • Niedrigere CPU Auslastung
  • Mehr Durchsatz bei vergleichbarer Konfiguration

Nutzung neuer Testmöglichkeiten, insbesondere für Integration-Tests

Auch für Integrationstests gibt es in ASP.NET (Core) nun wesentlich bessere Implementationsmöglichketen, verglichen mit dem "klassischen" .NET Framework. Im Gegensatz zu Komponententests, die einzelne Teile Ihrer Anwendung testen, prüfen Integrationstests das Zusammenspiel verschiedener Komponenten wie Routing, Middleware, Controller und Datenzugriffsschichten.
Ein bewährter Ansatz für Integrationstests in ASP.NET Core ist die Verwendung der Microsoft.AspNetCore.Mvc.Testing-Bibliothek. Diese bietet die Klasse WebApplicationFactory<TEntryPoint>, mit der ein Testserver erstellt werden kann, der HTTP-Anfragen an die API entgegennimmt.
Im Folgenden ist eine schrittweise Anleitung mit Beispielcode in C# dokumentiert, mit der beispielhaft Integrationstests für eine ASP.NET Core API eingerichtet und durchgeführt werden können.

Schritt 1: Einrichten des Testprojekts

  1. Erstellung neues Testprojekt:
    Neues xUnit-Testprojekt zur Solution hinzufügen. Dies kann über Visual Studio oder die .NET CLI erfolgen:
dotnet new xunit -o YourApi.Tests
  1. Projektreferenzen hinzufügen:
    Projektverweis auf API-Projekt hinzufügen, damit das Testprojekt auf die Program- oder Startup-Klasse zugreifen kann.
  2. Erforderliche NuGet-Pakete installieren:
    Notwendige Pakete im Testprojekt installieren:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk

 


Schritt 2: Integrationstests mit WebApplicationFactory

Beispielcode:
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Microsoft.AspNetCore.Mvc.Testing;
using YourApiNamespace; // Ersetzen Sie dies durch den Namespace Ihrer API

namespace YourApi.Tests
{
    public class IntegrationTests : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;

        public IntegrationTests(WebApplicationFactory<Program> factory)
        {
            _client = factory.CreateClient();
        }

        [Fact]
        public async Task Get_Endpoint_Returns_Success()
        {
            // Arrange
            var url = "/api/values"; // Passen Sie den Endpunkt entsprechend an

            // Act
            var response = await _client.GetAsync(url);

            // Assert
            response.EnsureSuccessStatusCode(); // Überprüft, ob der Statuscode 2xx ist
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            // Weitere Überprüfungen können hier hinzugefügt werden
        }

        [Fact]
        public async Task Post_Endpoint_Returns_Created()
        {
            // Arrange
            var url = "/api/values";
            var jsonContent = "{\"name\":\"Test Value\"}";
            var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

            // Act
            var response = await _client.PostAsync(url, content);

            // Assert
            Assert.Equal(HttpStatusCode.Created, response.StatusCode);
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            // Weitere Überprüfungen können hier hinzugefügt werden
        }
    }
}

Erläuterungen:

  • IClassFixture<WebApplicationFactory<Program>>:

    • Dieses Interface stellt eine gemeinsame Instanz von WebApplicationFactory für alle Tests bereit.
    • Für .NET 6 und früher ist  Program durch Startup zu ersetzen.

  • CreateClient():

    • Erstellt einen HttpClient, der Anfragen an den in-memory Testserver sendet.

  • Testmethoden ([Fact]):

    • Verwendet [Fact], um Testmethoden zu kennzeichnen.
    • Jede Methode sollte die Schritte Arrange, Act und Assert enthalten.

Schritt 3: Anpassen WebApplicationFactory (Optional)

Wenn beispielsweise eine In-Memory-Datenbank verwendet werdem soll, kann die WebApplicationFactory wie folgt erweitert werden:

Beispielcode:

using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using YourApiNamespace;

namespace YourApi.Tests
{
    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                // Entfernen der vorhandenen DbContext-Registrierung
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<YourDbContext>));

                if (descriptor != null)
                {
                    services.Remove(descriptor);
                }

                // Hinzufügen eines In-Memory-Datenbankkontexts
                services.AddDbContext<YourDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });

                // Erstellen des Service Providers
                var serviceProvider = services.BuildServiceProvider();

                // Initialisieren der Datenbank
                using (var scope = serviceProvider.CreateScope())
                {
                    var db = scope.ServiceProvider.GetRequiredService<YourDbContext>();
                    db.Database.EnsureCreated();

                    // Optionale Datenbank-Seeding-Methode
                    // SeedDatabase(db);
                }
            });
        }
    }
}

Erläuterungen:

  • CustomWebApplicationFactory:

    • Ermöglicht es, die Konfiguration des Testservers anzupassen.
    • Entfernt die vorhandene Datenbankkonfiguration und ersetzt sie durch eine In-Memory-Datenbank.

  • YourDbContext:

    • Bitte durch eigenen DbContext ersetzen.

Schritt 4: Verwenden CustomWebApplicationFactory in Tests

Beispielcode:

using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using YourApiNamespace;

namespace YourApi.Tests
{
    public class IntegrationTests : IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;

        public IntegrationTests(CustomWebApplicationFactory<Program> factory)
        {
            _client = factory.CreateClient();
        }

        [Fact]
        public async Task Get_Endpoint_Returns_Success()
        {
            // Arrange
            var url = "/api/values";

            // Act
            var response = await _client.GetAsync(url);

            // Assert
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
        }

        // Weitere Testmethoden...
    }
}

Schritt 5: Ausführen der Tests

Die Tests können mit dem folgenden Befehl ausgeführt werden:

dotnet test

Alternativ kann der Test-Explorer in Visual Studio verwendet werden.


Zusätzliche Hinweise

  • Authentifizierung:

    • Wenn die API Authentifizierung verwendet, muss diese für Tests möglicherweise gemockt werden.

  • Testdaten-Seeding:

    • Vor dem Test wäre die In-Memory-Datenbank mit erforderlichen Testdaten zu befüllen.

  • Umgebungsvariablen:

    • Ggf. Umgebung auf "Testing" setzen, um spezifische Konfigurationen zu laden (per ASPNETCORE_ENVIRONMENT-Umgebungsvariable).

Kompletter Beispielcode

CustomWebApplicationFactory.cs

using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using YourApiNamespace;

namespace YourApi.Tests
{
    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                // Entfernen der vorhandenen DbContext-Registrierung
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<YourDbContext>));

                if (descriptor != null)
                {
                    services.Remove(descriptor);
                }

                // Hinzufügen eines In-Memory-Datenbankkontexts
                services.AddDbContext<YourDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });

                // Erstellen des Service Providers
                var serviceProvider = services.BuildServiceProvider();

                // Initialisieren der Datenbank
                using (var scope = serviceProvider.CreateScope())
                {
                    var db = scope.ServiceProvider.GetRequiredService<YourDbContext>();
                    db.Database.EnsureCreated();

                    // Optionale Datenbank-Seeding-Methode
                    // SeedDatabase(db);
                }
            });
        }
    }
}

IntegrationTests.cs

using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using YourApiNamespace;

namespace YourApi.Tests
{
    public class IntegrationTests : IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;

        public IntegrationTests(CustomWebApplicationFactory<Program> factory)
        {
            _client = factory.CreateClient();
        }

        [Fact]
        public async Task Get_Endpoint_Returns_Success()
        {
            // Arrange
            var url = "/api/values";

            // Act
            var response = await _client.GetAsync(url);

            // Assert
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
        }

        // Weitere Testmethoden...
    }
}

Fazit

Integrationstests mittels WebApplicationFactory und HttpClient sind eine gute Möglichkeit, die gesamte Anforderung-Antwort-Pipeline zu testen und sicherzustellen, dass alle Komponenten nahtlos zusammenarbeiten.

Weiterführende Links:

Über uns

Ein erfahrenes Entwicklerteam, das mit Leib und Seele Software erstellt.

evanto logo

Kontaktdaten

Brunnstr. 25,
Regensburg

+49 (941) 94592-0
+49 (941) 94592-22

Statistik