62 KiB
\cleardoublepage
Konstantin Hintermayer
Einleitung
Motivation
Gegenstand der Diplomarbeit ist die Entwicklung modularer Webanwendungen mit Blazor und Oqtane. Aufbauend auf fundierten Kenntnissen in der Fullstack-Entwicklung (React, Node.js, Golang), welche privat bei diversen Projekten gesammelt worden sind, fokussiert sich die Arbeit auf die Architekturvorteile des .NET-Stacks. Besonders im Fokus stehen die Konsistenz durch statische Typisierung sowie das Zusammenspiel modularer Komponenten in verteilten Systemen.
Mein Untersuchungsanliegen: Inwieweit optimiert die Integration von Blazor und dem Oqtane-Framework die Konsistenz und Wartbarkeit modularer Web-Architekturen im Vergleich zu den für mich gewohnten Technologie-Stacks (React/Node.js)?
Auftrag und persönliche Aufgabenstellungen
Mein Aufgabenbereich in diesem Projekt war vielseitig und umfasste sowohl leitende als auch tiefgreifende technische Aspekte. Die Rollen lassen sich in drei Kerngebiete unterteilen:
Projektleitung und Organisation (Product Owner)
\
Als Product Owner war ich für die Definition der Produktvision und die Priorisierung des Backlogs verantwortlich. Dies beinhaltete:
- Anforderungsmanagement: Erhebung und Strukturierung der Anforderungen in Zusammenarbeit mit den Betreuern und dem Team.
- Sprint-Planung: Organisation der 14-tägigen Sprints in YouTrack, um einen kontinuierlichen Entwicklungsfluss sicherzustellen.
- Qualitätssicherung: Definition der Definition of Done (DoD) und Durchführung von Code-Reviews zur Einhaltung von Architekturstandards.
Infrastruktur und Systemarchitektur
\
Ein wesentlicher Teil meiner Arbeit lag in der Bereitstellung der technischen Basis für das gesamte Team:
- Deployment-Strategie: Konzeption und Umsetzung der Server-Infrastruktur auf Basis von Debian Linux und NginX als Reverse-Proxy.
- Datenbankdesign: Entwurf des relationalen Datenmodells in PostgreSQL, inklusive der Absicherung durch SSL/TLS.
- CI/CD-Pipeline: Automatisierung der Build- und Deployment-Prozesse mittels Gitea Actions für eine effiziente Integration der Teambeiträge.
Modulentwicklung
\
Zusätzlich zur Infrastruktur habe ich drei zentrale Module für den AlumniHub entworfen und implementiert:
- Anmeldetool (EventRegistration): Ein Modul zum Verwalten von Anmeldungen für Veranstaltungen. Es ermöglicht die einfache Erstellung von Einladungen sowie die Verwaltung von Anmeldungen.
- Reporting-System: Ein generisches System zur Meldung von Inhalten, das nach dem Open-Closed-Prinzip entworfen wurde, um eine einfache Erweiterbarkeit für alle anderen Module zu bieten.
- Schwarzes Brett (BlackBoard): Ein Community-Modul für den Austausch von Informationen, inklusive eines automatisierten E-Mail-Digest-Systems zur Nutzerbenachrichtigung.
Anforderungen an das entwickelte Modul bzw. die Funktionalität
Modulanforderungen / funktionale Anforderungen
Die funktionalen Anforderungen beschreiben die spezifischen Dienste und Funktionen, die das System für die Benutzer bereitstellen muss. Im Rahmen der individuellen Aufgabenstellung wurden Anforderungen für fünf Teilbereiche definiert:
Anmeldetool (EventRegistration)
\
Das Ziel des Anmeldetools ist die effiziente Organisation von Vereinsveranstaltungen.
- Veranstaltungsmanagement: Administratoren müssen in der Lage sein, neue Veranstaltungen anzulegen. Dabei müssen Name, Beschreibung, Datum, Zeit und Ort festlegbar sein.
- Anmeldeprozess: Registrierte Benutzer müssen sich für eine Veranstaltung an- oder abmelden können.
- Teilnehmerliste: Für jede Veranstaltung muss eine Übersicht der Rückmeldungen (Anmeldungen/Absagen) für Administratoren einsehbar sein.
- Statistische Auswertung: Die Rückmeldungen sollen grafisch (z. B. als Tortendiagramm) aufbereitet werden, um die Planung zu erleichtern.
Reporting-System
\
Das Reporting-System dient der Qualitätssicherung von Benutzerinhalten über alle Module hinweg.
- Inhalte melden: Benutzer müssen die Möglichkeit haben, unangemessene Einträge (z. B. am Schwarzen Brett) zu melden. Dabei muss ein Grund für die Meldung angegeben werden können.
- Generische Schnittstelle: Das System muss so entworfen sein, dass beliebige andere Module (z. B. Hall of Fame) die Meldefunktion ohne Änderungen am Kern des Reporting-Systems einbinden können.
- Administrations-Dashboard: Gemeldete Inhalte müssen in einer zentralen Übersicht für Administratoren aufgelistet werden.
- Moderationsaktionen: Administratoren müssen gemeldete Inhalte löschen, die Meldung ignorieren oder den Ersteller verwarnen/sperren können.
Schwarzes Brett (BlackBoard)
\
Das Schwarze Brett fungiert als digitale Kommunikationsplattform für den Verein.
- Beiträge erstellen: Benutzer müssen Textbeiträge erstellen können. Die Formatierung der Texte soll über einen Rich-Text-Editor möglich sein.
- Bilder-Upload: Zu jedem Beitrag soll optional ein Bild hochgeladen werden können, welches automatisch für die Vorschau skaliert wird.
- Benachrichtigungs-System: Das System soll regelmäßig (täglich/wöchentlich) eine Zusammenfassung neuer Beiträge per E-Mail an alle interessierten Mitglieder versenden.
Mass Mailing (Admin)
\
Dieses Modul ermöglicht die direkte Kommunikation des Vorstandes mit allen Mitgliedern.
- Rundmails verfassen: Administratoren müssen E-Mails mit Betreff und Inhalt an alle registrierten Benutzer verfassen können.
- Personalisierung: Die E-Mails sollen automatisch mit dem Namen des Empfängers personalisiert werden können.
- Verspätetes Senden: Aufgrund von Versandlimits des Providers (Brevo) müssen die E-Mails in eine Warteschlange eingereiht und zeitversetzt in Batches versendet werden.
Token Lifetime Management (Admin)
\
Dies dient der Konfiguration sicherheitsrelevanter Parameter.
- Gültigkeitsdauer: Administratoren müssen die Gültigkeitsdauer für temporäre Links (z. B. Passwort-Reset, E-Mail-Bestätigung) über eine grafische Oberfläche anpassen können.
- Einstellungs-Persistenz: Die geänderten Werte müssen dauerhaft gespeichert werden und sofort für neue Token-Generierungen wirksam sein.
Use Cases
Um die Interaktion der Benutzer mit den Modulen zu verdeutlichen, wurden folgende Use Cases definiert:
| ID | Name | Akteur | Beschreibung |
|---|---|---|---|
| 1 | Veranstaltung erstellen | Administrator | Ein Administrator legt ein neues Absolvententreffen mit Ort und Datum an. |
| 2 | Zu Event anmelden | Mitglied | Ein Absolvent bestätigt seine Teilnahme an einem Event über die Weboberfläche. |
| 3 | Inhalt melden | Mitglied | Ein Benutzer meldet einen beleidigenden Post am Schwarzen Brett über den "Melden"-Button. |
| 4 | Meldung bearbeiten | Moderator | Ein Vorstandsmitglied sichtet eine Meldung und löscht den entsprechenden Beitrag. |
| 5 | Rundmail versenden | Administrator | Der Vorstand erstellt eine Einladung zur Generalversammlung für alle 500 Mitglieder. |
Table: Wesentliche Use Cases der entwickelten Module
Es ist zu beachten, dass es sich hierbei um eine Auswahl handelt und nicht alle Use Cases der Module abgebildet werden.
Technologien
Mein Aufgabenbereich umfasst einerseits die Entwicklung eigener Module, sowie das Bereitstellen des Services.
Auswahlverfahren
Entscheidungsfindung CMS
\
Auch steht die Wahl der Programmiersprache und des CMS an. Nachdem wir im Unterricht fast ausschließlich mit C# entwickelt haben und nicht in eine komplett unbekannte Entwicklungsumgebung abdriften wollten, haben wir uns für die Webentwicklung mit ASP.NET Core 9 (Upgrade im Laufe der Diplomarbeit auf .NET Core 10) und dem CMS Oqtane entschieden. Auch hier gab es einige Kandidaten:
- Piranha CMS
Piranha erscheint auf den ersten Blick nicht so flexibel wie Oqtane, es basiert auf .NET 8.0 und wird nicht so aktiv gewartet.
- Umbraco
Bei Umbraco muss viel in der Admin-Oberfläche vom CMS gearbeitet werden, im Großen und Ganzen wirkt dieses CMS nicht so flexibel. Die Dokumentation wirkt auf den ersten Blick sehr gut!
- DNN / Dot Net Nuke
Dieses CMS ist der Platzhirsch. Es wird von der DNN Foundation gewartet und arbeitet mit dem Dotnet Framework, welches nicht unter Linux läuft. Ein Windows Server ist im Betrieb teurer und in der Absicherung aufwändiger.
- Oqtane
Oqtane wirkt sehr modular und flexibel, auch innerhalb von Modulen kann man alle Funktionen des ASP.NET Core Frameworks verwenden. Die Dokumentation wirkt nicht besonders gut, aber ausreichend. Die enthaltenen Fehlinformationen und mangelnden Anleitungen für den Betrieb mit Linux sind erst im Nachhinein aufgefallen.
Insbesondere aufgrund seiner sehr hohen Flexibilität haben wir uns am Ende für Oqtane entschieden.
Entscheidungsfindung restliche Infrastruktur
\
Als Betriebssystem habe ich mich für Linux entschieden, einfach, da ich mit Linux im Serverumfeld die meisten und besten Erfahrungen gemacht habe.
Im Bereich der Datenbanken musste ich mir ein paar Fragen stellen:
- Auf welche Art Datenbank setzen wir? SQL, NoSQL, Graph, ...
- Mit welcher speziellen Implementierung bekommen wir Support und bei welcher haben wir Vorwissen im Team?
- Ist das auserkorene System kompatibel mit dem CMS, auf dem wir aufbauen?
Es war von Anfang an klar, dass es ein SQL-basiertes System wird, da wir im Team nur mit SQL-basierten Systemen Erfahrungen haben. Außerdem unterstützt unser CMS (Oqtane) nur SQL-basiierte Systeme. In der Linux-Welt kommen jetzt nur noch ein paar Datenbankmanagementsysteme in die Auswahl: PostgreSQL, MySQL / MariaDB, SQLite. Da ist die Wahl auf PostgreSQL gefallen. Grund dafür war meine Vorerfahrung mit diesem Datenbankmanagementsystem, welche ich in meinem Nebenjob erlangt habe.
Beschreibung und Architektur von Oqtane
Oqtane ist ein Framework und CMS zur Entwicklung von Webseiten mithilfe von ASP.NET und Blazor. [@oqtane_about] Ein Oqtane-System besteht aus mehreren Komponenten.
In dieser Diplomarbeit fokussieren wir uns hauptsächlich auf Themes und Modules, aber es gibt auch Language Packs und Pure Extensions. [@oqtane_docs_extensions]
Ein Module (Modul) soll neue Funktionalitäten in das CMS hinzufügen und ein Theme soll die ganze Gestaltung der Webseite (die Shell) festlegen. [@oqtane_docs_extensions]
Architektur eines Moduls
\
Ein Modul in Oqtane besteht aus vier Projekten:
-
Im Server-Projekt liegt Sourcecode, welcher serverseitig ausgeführt werden soll. Dazu gehören unter anderem alle Repositories, Controller, Manager, Migrationen und Server-Services und Server-Startuplogik.
-
Im Client-Projekt liegen Code und Razor-Komponenten für den Client. Also Client-Startuplogik, Client-Services, Ressourcendateien (.resx), die Komponenten / das User-Interface und die Moduldefinitionen für jedes Modul.
-
Im Shared-Projekt wird geteilter Sourcecode abgelegt, der server- und clientseitig verwendet wird. In der Praxis bleibt es hierbei bei den EntityFramework-Modellen zum Speichern der Daten im Arbeitsspeicher.
-
Im Package-Projekt findet man Skripte zum Debuggen und Releasen eines Moduls und die NuGet-Spezifikation.
- Beim Debug werden die DLLs, PDBs und statischen Assets wie Skripte und Stylesheets der drei anderen Projekte in den bereits gebauten Oqtane.Server
oqtane.framework/oqtane.server/bin/debug/net10.0/...kopiert. - Beim Release wird ein NuGet-Paket erstellt und unter oqtane.framework/oqtane.server/Packages abgelegt. Dort abgelegte NuGet-Pakete werden beim nächsten Start des Oqtane-Servers installiert (Datenbank-Migrationen werden gemacht und die Pakete entpackt).
- Beim Debug werden die DLLs, PDBs und statischen Assets wie Skripte und Stylesheets der drei anderen Projekte in den bereits gebauten Oqtane.Server
Zusammenspiel der Infrastruktur
In diesem Kapitel erkläre ich, wie die ausgewählten Komponenten zusammenspielen.
NginX as Reverse Proxy
\
NginX fungiert in unserer Infrastruktur als Reverse Proxy. Ein Reverse Proxy nimmt Anfragen aus dem Internet entgegen und leitet sie an interne Server (wie Kestrel) weiter. Dies bietet mehrere Vorteile:
- Sicherheit: Die interne Applikation ist nicht direkt dem Internet ausgesetzt.
- SSL-Terminierung: Nginx übernimmt die rechenintensive Verschlüsselung (HTTPS), während die Applikation dahinter über einfaches HTTP kommuniziert.
- Statische Inhalte: Nginx kann statische Dateien (Bilder, CSS) effizienter ausliefern als ein Applikationsserver.
Wir verwenden Nginx für die SSL-Terminierung. Das Zertifikat wird von Let’s Encrypt bereitgestellt und mittels HTTP-Challenges und dem Certbot auf dem Server aktualisiert.
Hier ist ein Auszug der NginX-Konfiguration (nginx.conf) für den AlumniHub:
server {
listen 443 ssl;
server_name alumni.example.com;
ssl_certificate /etc/letsencrypt/live/alumni.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/alumni.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Oqtane selbst läuft als Systemd-Service im Kestrel-Backend. Kestrel ist ein kleiner Webserver, welcher in das ASP.NET Core Framework eingebaut worden ist. Oqtane (bzw. der ASP.NET Core Server "Kestrel") hört auf der Loopback-IP und Port 5000. Damit ist Oqtane nur durch Nginx erreichbar. PostgreSQL ist die Datenbank in dem System: Sie hört wieder auf der Loopback-IP und Port 5432. In der folgenden Grafik ist das System schematisch dargestellt.
sequenceDiagram
%%| filename: sequence-diagram-system-architecture
%%| fig-cap: Schematische Darstellung der Systemarchitektur
participant browser as Client
participant nginx as NginX
participant oqtane as Oqtane
participant db as PostgreSQL
browser->>+nginx: HTTPS => 0.0.0.0:443
nginx->>nginx: SSL Terminierung
nginx->>+oqtane: HTTP => 127.0.0.1:5000
oqtane->>+db: SQL => 127.0.0.1:5432
db-->>-oqtane: SQL
oqtane-->>-nginx: HTTP
nginx-->>-browser: HTTPS
Zusätzlich gab es einen Administrationszugang zu den Servern, welcher über SSH möglich war. Dieser wurde für die Installation und Konfiguration der einzelnen Komponenten verwendet. Der SSH Service ist in jeder Umgebung anders erreichbar gewesen.
| Umgebung | Administrationszugang |
|---|---|
| Hetzner | Wireguard |
| Schule | Highport |
| LiveDesign | IPSEC VPN |
Table: SSH Zugänge in den unterschiedlichen Umgebungen
Die VPN-basierten Zugänge sind tendenziell schwieriger zu finden und auszunutzen, während die Lösung in der Schule mittels Highport den SSH-Service öffentlich erreichbar macht. Durch den Highport ist der SSH-Service schwieriger zu finden. Zur Authentifizierung mit SSH verwenden wir SSH-Keys, da diese durch ihre Komplexität sicherer sind als Passwörter.
Entwicklung mit ASP.NET
Blazor [@wikipedia_blazor]
\
Blazor ist ein kostenloses und quelloffenes Web-Framework, welches es ermöglicht, Benutzeroberflächen für Web-Browser basierend auf C# und HTML zu erstellen. Es wird von Microsoft als Teil des ASP.NET Core Frameworks entwickelt.
Blazor hat mehrere Hosting-Modelle:
| Hosting-Modell | Ausführungsort | Interaktivität | Kommunikation |
|---|---|---|---|
| Static Server | Server | Keine | HTTP (Initialer Load) |
| Interactive Server | Server | Hoch (Echtzeit) | SignalR / WebSockets |
| Interactive WebAssembly | Client (Browser) | Hoch (Lokal) | REST API / HTTP |
| **Interactive Auto ** | Server & Client (ab zweitem Besuch) | Hoch (Echtzeit / Lokal) | SignalR / Websockets / RestAPI / HTTP |
| Hybrid | Native App (WebView) | Hoch (Lokal) | n/a (Mutterprozess) |
Table: Vergleich der Blazor Hosting-Modelle
Im Rahmen dieser Diplomarbeit haben wir uns für den Rendermodus Interactive Server entschieden. Dieser Modus ist für Oqtane die Standardeinstellung und bietet eine gute Balance zwischen Leistung und Benutzerfreundlichkeit. Das ist für unseren Anwendungsfall auch die Empfehlung von Oqtane selbst. [@oqtane_docs_rendermodes]
Razor-Komponenten (in dieser Arbeit sowie umgangssprachlich auch oft nur Komponenten genannt) sind der Grundbaustein für Blazor Web Apps. Sie bestehen aus HTML, welches mit der Verwendung von Inline-C# beeinflusst werden kann. Blazor stellt sicher, dass das gerenderte Markup aktualisiert wird, wenn sich der Status der Komponente ändert. Dieser Code kann entweder vollständig in einer .razor-Datei liegen oder in einer separaten Code-Behind-Datei. Inline-C#-Code wird mithilfe von @-Zeichen markiert. Hier ist ein Beispiel für einen einfachen Counter:
<h1>Counter</h1>
<p>Count: @count</p>
<button @onclick="Increment">Increment</button>
@code
{
private int count = 0;
private void Increment()
{
count++;
}
}
Mit@count in Zeile 3 wird der Wert der Variablen count in den <p> Tag mit eingebaut. Mit @onclick="Increment" in Zeile 5 wird die onclick Property vom <button>Tag auf die Increment Methode im C# Code gesetzt. Der @code Block in Zeile 7 ist der C# Code, welcher diese Komponente dynamisch macht. Hier ist die Variable count und die Methode Increment definiert, welche dieser Komponente interaktiv macht.
Razor hat auch eine Reihe an Keywords, wie zum Beispiel (nur auszugsweise bzw. die, die wir verwendet haben):
- namespace: Gibt den aktuellen Namespace in der Razor Datei an.
- inherits: Gibt die Superklasse der generierten C# Klasse an.
- using: Gibt die im C# Code benutzen/verfügbaren Namespaces an
- foreach: Für Wiederholungen im Markup
- if: Für Verzweigungen im Markup
Kommunikation zwischen Front- und Backend
\
Die Interaktion zwischen Client und Server folgt in Oqtane einem klaren Architekturmuster, das je nach Render-Modus unterschiedliche Technologien nutzt.
- SignalR (Interactive Server): Für Echtzeit-Interaktionen nutzt Oqtane SignalR. Dabei wird eine persistente WebSocket-Verbindung (oder Fallbacks wie Long-Polling) aufgebaut. Zustandsänderungen im UI lösen C#-Events auf dem Server aus, und das resultierende "Diff" des DOMs wird zurück an den Client gestreamt.
- REST API (WebAssembly/Shared): Bei Modulen, die Daten asynchron laden (im WebAssembly-Modus), kommuniziert der Client über einen
HttpClientmit dem Controller.
Der folgende Ablauf zeigt die Kommunikation bei einer typischen Datenabfrage in einem Oqtane-Modul:
sequenceDiagram
%%| filename: sequence-diagram-data-query
%%| fig-cap: Schematische Darstellung der Systemarchitektur
participant C as Blazor Client (Razor)
participant S as Oqtane Server (Controller)
participant R as Repository (EntityFramework)
participant D as PostgreSQL
C->>+S: HTTP GET /api/[Module]/[Id]
S->>+R: GetEntity(Id)
R->>+D: SQL Query
D-->>-R: Data Result
R-->>-S: Entity Model
S-->>-C: JSON Response
Durch diese Abstraktion bleibt die Geschäftslogik im Backend (Repository/Controller) gekapselt, während das Frontend lediglich die Daten präsentiert und Benutzereingaben entgegennimmt. Dies unterstützt die Testbarkeit, da Repositories unabhängig vom UI getestet werden können.
Dependency Injection
Dependency Injection ist ein Entwurfsmuster, bei dem die Abhängigkeiten eines Objekts nicht von diesem selbst erzeugt, sondern von außen „injiziert“ werden.
Wie der Software-Architekt Martin Fowler, der den Begriff im Jahr 2004 maßgeblich prägte, beschreibt, geht es im Kern darum, die Erzeugung von Objekten von deren Nutzung zu trennen [@fowler_dependency_injection]. Anstatt dass eine Klasse ihre Hilfsobjekte mittels des new-Operators selbst instanziiert, werden ihr diese meist über den Konstruktor zur Verfügung gestellt.
In den folgenden beiden Kapiteln wird das Dependency Inversion Principle und das Microsoft Dependency Injection Framework genauer vorgestellt.
Dependency Inversion Principle [@ms_dependency_inversion] [@logrocket_dependency_inversion]
\
Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines der fünf SOLID-Prinzipien in der Softwareentwicklung.
Das DIP unterscheidet zwischen high-level und low-level Modulen.
- Die High-Level-Module beschreiben die Applikations- / Businesslogik, ohne direkt mit den Low-Level-Modulen zu interagieren, sondern lediglich auf Abstraktionen. [@oodesign_dependency_inversion]
- Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die Low-Level-Implementierung sollen gemäß der Abstraktionsschicht implementiert werden. [@oodesign_dependency_inversion]
Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph-Modell.
architecture-beta
%%| filename: architecture-beta
%%| fig-cap: DIP: Direct Dependency Graph
service a(mdi:package-variant-closed)[Klasse A]
service b(mdi:package-variant-closed)[Klasse B]
a:R --> L:b
Bei diesem Beispiel ist die Klasse A ein high-level Modul, welches direkt auf die Klasse B referenziert, was das DI-Prinzip verbietet. Das Problem dabei: Die einzelnen Klassen sind eng gekoppelt, was das Austauschen von B mit einer anderen Klasse unmöglich macht. Genau dieses Problem wird vom DIP gelöst.
architecture-beta
%%| filename: architecture-beta
%%| fig-cap: DIP: Dependency Inversion Principle
service a(mdi:package-variant-closed)[Klasse A]
service b(mdi:package-variant-closed)[Klasse B]
service ib(mdi:car-clutch)[Interface B]
a:R --> L:ib
ib:R <-- L:b
Das High-Level-Modul ruft lediglich eine Abstraktion eines Low-Level-Moduls auf, welche von einem, oder mehreren Low-Level-Modulen implementiert wurde. Für das High-Level-Modul ist es hier egal, welches Low-Level-Modul die Implementierung bereitstellt. Dadurch erhält man einen viel modulareren Aufbau in der Software. Die einzelnen Module sind auch leichter austauschbar und testbar. Genau diese Modularität macht Dependency Injection möglich.
Microsoft Dependency Injection Framework
\
Dependency Injection ist in .NET genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. [@ms_di_overview]
Alle Dependencies werden in einem Service-Container zur Verwaltung registriert. .NET hat einen eingebauten Service-Container (eine Implementierung des IServiceProvider). [@ms_di_overview]
Das Dependency Injection Framework verwaltet alle Instanzen. Nach Bedarf werden Instanzen erstellt, oder wieder entsorgt (sofern das Service nicht mehr gebraucht wird). Beim Instanziieren einer Klasse werden alle im Konstruktor erwarteten Dependencies bereitgestellt, bzw. selbst instanziiert und danach bereitgestellt. [@ms_di_overview]
Hier ein Beispiel aus der Dokumentation von Microsoft: [@ms_di_overview]
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
using IHost host = builder.Build();
host.Run();
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
public interface IMessageWriter
{
void Write(string message);
}
public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
Das ist ein simples Beispiel, welches Teile des DI Frameworks zeigt. Wir haben einen Service (Klasse Worker), ein Dependency (Klasse MessageWriter) und eine Abstraktionsebene, von dem Dependency (Interface IMessageWriter).
Bei Programmstart wird zuerst manuell der Service-Container erstellt, danach alle Module registriert (entweder als HostedService, oder als Modul mit einer spezifischen Lifetime (Scoped, Transient, Singleton)).
Mit dem Aufruf von builder.Build() wird intern ein Dependency Graph erstellt und mit host.Run() wird versucht die Klasse Worker zu instanziieren und zu starten. Nachdem Worker ein Dependency auf IMessageWriter hat, wird über den zuvor erstellten Dependency-Graph die Implementierung von IMessageWriter gesucht. Jetzt wird MessageWriter instanziiert und dem Konstruktor von Worker übergeben, damit seine Dependencies befriedigt werden.
So sieht der Abhängigkeitsgraph bei diesem Beispiel aus.
%%| fig-cap: DIP: Dependency Inversion Principle Beispiel
architecture-beta
service a(mdi:package-variant-closed)[Worker]
service b(mdi:package-variant-closed)[MessageWriter]
service ib(mdi:car-clutch)[IMessageWriter]
a:R --> L:ib
ib:R <-- L:b
Continuous Integration
Gitea, das Versionskontrollsystem dieser Diplomarbeit, hat einen Continuous-Integration-System eingebaut. Im Kern ist es baugleich zu den GitHub-Pipelines. Man kann im .gitea/workflow Ordner .yml Dateien ablegen, welche dann das Verhalten der Workflows definieren.
Man kann definieren auf welcher Änderung im Git Repository die Pipeline losgetreten wird (Keyword: on) und entweder eigene Kommandos aufreihen, oder auf bestehende actions zurückgreifen, welche dann der Reihe nach ausgeführt werden (Keyword: jobs).
Die meisten Pipelines sind folgendermaßen aufgebaut: Clone -> Checkout -> Submodule-Checkout (optional) -> Dependencies einrichten (zum Beispiel das dotnet SDK) -> Build ausführen -> Release erstellen und Artefakte veröffentlichen (z. B. in Registries). Aber man kann auch andere Dinge tun, z. B. mithilfe von künstlicher Intelligenz Code und Dokumentation überprüfen.
Anwendungen von Gitea Actions bei dieser Diplomarbeit:
- APT-Package Repository: Zum Bauen von Oqtane und allen Modulen, Verpacken in ein .deb-Paket und Pushen in die Registry.
- Interfaces-Projekt: Zum Bauen vom Interfaces-Projekt, Verpacken in ein NuGet-Paket und Pushen in die Registry. Die Versionierung des NuGet-Pakets erfolgt dabei automatisiert über Git-Tags, was eine konsistente Verknüpfung zwischen Quellcode-Stand und Paketversion sicherstellt.
- ursprünglich: oqtane.framework: Zum Bauen und Verpacken in einen Docker-Container und Pushen in die Registry.
- PM Repository: Zum automatischen Überprüfen der Dokumente unter anderem mithilfe von KI, wie zum Beispiel Gemini.
Ein Beispiel für eine Konfiguration einer Gitea Action:
name: build-debian-package
on:
push:
tags:
- "*"
jobs:
build:
name: Build the debian package
runs-on: ubuntu-latest
steps:
- name: "Git clone"
run: git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
- name: "Git checkout"
run: git checkout "${{ gitea.sha }}"
- name: "Submodules auschecken"
run: git submodule update --init
- name: "Dotnet SDK einrichten"
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: "Configure nuget source"
run: dotnet nuget add source --name DAV --username kocoder --password ${{ secrets.REGISTRY_TOKEN_KOCODER }} https://git.kocoder.xyz/api/packages/Diplomarbeit-Absolventenverein/nuget/index.json --store-password-in-clear-text
- name: "Build .deb"
run: ./run-build.sh "${{ gitea.ref_name }}" "./alumnihub" "Release"
- name: "Upload .deb"
run: curl --user kocoder:${{ secrets.REGISTRY_TOKEN_KOCODER }} --upload-file ./alumnihub.deb https://git.kocoder.xyz/api/packages/Diplomarbeit-Absolventenverein/debian/pool/trixie/main/upload
- name: "Create release"
uses: akkuman/gitea-release-action@v1
with:
files: |-
./alumnihub.deb
./alumnihub/opt/alumnihub/Packages/*.nupkg
Die Konfiguration führt teilweise vorgefertigte Actions aus, wie zum Beispiel das Einrichten des .NET SDKs oder das Erstellen eines Releases. Aber es werden auch eigene Skripte ausgeführt, wie zum Beispiel das Bauen des .deb-Pakets. Dieses ist im Repository unter ./run-build.sh zu finden. Dieses Skript ist für die Automatisierung des Build-Prozesses zuständig und kümmert sich um das Kompilieren der Anwendung, das Erstellen des Debian-Pakets und das Hochladen in die Registry. Abgelegt ist es im APT-Package-Repository unter ./gitea/workflows.
Debian Paket
Um die Anwendung und ihre Abhängigkeiten konsistent auf dem Zielserver (Linux/Hetzner) zu installieren, wurde ein eigenes Debian-Paket (.deb) erstellt. Das Debian-Paketformat bietet den Vorteil, dass es Metadaten über Versionen, Abhängigkeiten und Installationsskripte (Maintainer Scripts) enthält.
Struktur eines Debian-Pakets
\
Ein .deb-Paket ist im Grunde ein ar-Archiv, das drei wesentliche Bestandteile enthält:
- debian-binary: Eine Textdatei mit der Versionsnummer des Paketformats.
- control.tar.gz: Enthält die Metadaten (
control-Datei) und Skripte, die vor oder nach der Installation ausgeführt werden (z. B.postinstzum Starten des Systemd-Services). - data.tar.gz: Enthält die eigentlichen Anwendungsdateien (die kompilierten Oqtane-DLLs und statischen Assets), die in das Zielverzeichnis (z. B.
/opt/alumnihub) entpackt werden.
Automatisierung im Build-Prozess
\
Der Bau des Pakets erfolgt vollautomatisch in der Gitea-CI-Pipeline. Dabei werden die folgenden Schritte durchlaufen:
- Dotnet Publish: Kompilieren der Anwendung für Linux-x64.
- Paketierung: Erstellen der Verzeichnisstruktur gemäß dem FHS (Filesystem Hierarchy Standard).
- dpkg-deb: Aufruf des Standard-Werkzeugs
dpkg-deb --build, um das fertige Paket zu schnüren. Dabei wird auch hier ein Git-Tag als Grundlage für die Paketversion verwendet. - Publish: Das Paket wird in die Gitea Package Registry hochgeladen und steht dort für das Deployment via
aptzur Verfügung.
Durch diesen Prozess wird sichergestellt, dass jede Version der Software eindeutig identifizierbar und einfach rückrollbar (Rollback) ist.
Projektmanagement & Tools
Scrum
Zum Verwalten des Projektes haben wir uns an den Grundprinzipien von Scrum orientiert, um flexibel auf Änderungen reagieren zu können. Die Arbeit wurde in 14-tägige Sprints unterteilt.
- Sprint Planning: Zu Beginn jedes Sprints wurden die Aufgaben aus dem Product Backlog in das Sprint Backlog übernommen.
- Dailies/Weekly: Wir hielten wöchentliche Treffen ab, um den Fortschritt zu synchronisieren und Blocker zu identifizieren.
- Sprint Review: Am Ende eines Sprints wurden die funktionalen Module präsentiert und der Stand der Diplomarbeit mit dem Betreuer besprochen.
- Sprint Retrospective: Am Ende eines Sprints wurde der Sprint reflektiert und es wurden Maßnahmen zur Verbesserung des Prozesses abgeleitet.
- Definition of Done: Zu Beginn der Diplomarbeit haben wir uns auf eine Definition of Done geeinigt, welche im Laufe der Diplomarbeit immer wieder angepasst wurde.
Mehr zum Projektmanagementprozess steht im allgemeinen Teil Projektmanagement mit Scrum.
Projektmanagement-Tools (YouTrack & Gitea)
Für die initiale Planung und die ersten Sprints wurde YouTrack von JetBrains eingesetzt. Im Vergleich zu einfachen To-Do-Listen bietet YouTrack mächtige Features für Softwareteams wie agile Boards, Time Tracking und Gantt-Diagramme.
Im weiteren Verlauf der Diplomarbeit sind wir jedoch auf die integrierten Projektmanagement-Funktionen von Gitea umgestiegen. Siehe Issues. Dieser Wechsel erfolgte, um den gesamten Entwicklungsprozess — von der Aufgabenverwaltung (Issues/Boards) über die Versionskontrolle bis hin zur CI/CD-Pipeline (Gitea Actions) — an einem zentralen Ort zu bündeln. Dies verbesserte die Übersichtlichkeit und reduzierte den administrativen Aufwand für die Pflege unterschiedlicher Systeme.
Git
Git fungierte im Rahmen dieser Diplomarbeit als zentrales Werkzeug zur Versionskontrolle und Koordination sämtlicher Entwicklungsschritte. Die wichtigste Komponente stellt dabei der Commit dar, welcher als eindeutiger Snapshot des Projektzustands dient und über eine spezifische Commit-SHA (Secure Hash Algorithm) referenziert werden kann. Um eine lückenlose Nachvollziehbarkeit zu gewährleisten, wurde für jeden abgeschlossenen Arbeitsschritt ein eigener Commit erstellt.
Im Gegensatz zu zentralisierten Systemen wie SVN, welches wir im Unterricht beigebracht bekommen haben, speichert Git die vollständige Historie eines Projekts lokal auf dem Rechner jedes Beteiligten. Dies ermöglicht nicht nur ein Offline-Arbeiten, sondern bietet auch eine hohe Ausfallsicherheit.
Der Workflow in Git basiert auf drei Hauptbereichen:
- Working Directory: Der aktuelle Zustand der Dateien auf der Festplatte.
- Staging Area (Index): Eine Zwischenebene, in der Änderungen für den nächsten Snapshot vorgemerkt werden.
- Repository (HEAD): Der dauerhafte Speicher der versionierten Stände (Commits).
Eines der mächtigsten Features von Git ist das Branching. Es erlaubt es, isolierte Entwicklungszweige zu erstellen, um neue Features oder experimentelle Analysen zu implementieren, ohne die Stabilität des Hauptzweigs main zu gefährden.
Sobald eine Änderung erfolgreich getestet wurde, wird sie durch einen Merge wieder in den Hauptzweig integriert. Sollten dabei widersprüchliche Änderungen an denselben Dateizeilen auftreten, unterstützt Git den Nutzer bei der Auflösung dieser sogenannten Merge-Konflikte.
Für die Sicherung und Zusammenarbeit wurde im Rahmen dieser Arbeit Remote-Repositories auf einer Gitea Instanz verwendet. Durch die Befehle push und pull wird der lokale Stand mit dem Server synchronisiert. Dies gewährleistet eine konsistente Datenbasis und dient gleichzeitig als kontinuierliches Backup des Projektfortschritts.
Gitea
Als schlanke und selbst gehostete Open-Source-Alternative zu Plattformen wie GitHub oder GitLab wurde für die Verwaltung der Repositories Gitea eingesetzt. Hauptgrund für die Benutzung von Gitea war meine bereits aufgesetzte Instanz, welche schon mehrere Jahre in Verwendung war und mit der es im Team auch schon Erfahrungen gab. Gitea bietet neben der reinen Git-Verwaltung essenzielle Werkzeuge für den Software-Lebenszyklus, wie ein integriertes Issue-Tracking, Code-Reviews über Pull-Requests sowie eine Benutzerverwaltung, und unterstützt somit die strukturierte Umsetzung der Diplomarbeit im Team.[@gitea_docs]
Repositories
\
Ein Repository bildet den zentralen Speicherort für einen Projektteil. In Gitea wurden separate Repositories für die einzelnen Module und Themes, das Oqtane Framework, Skripte, die Dokumentation angelegt. Ein zusätzliches Repository bindet alle übrigen Quellcode-Repositories als Submodule ein, das macht die Einrichtung einer neuen Entwicklungsumgebung sehr komfortabel. Dies ermöglichte eine saubere Trennung der verschiedenen Projektkomponenten. [@gitea_docs]
Issues
\
Zur Aufgabenplanung und Fehlerverfolgung wurde das integrierte Issue-System genutzt. Jede anstehende Aufgabe oder entdeckte Schwachstelle wurde als „Issue“ erfasst, einem Verantwortlichen zugewiesen und mit Labels (z. B. „Bug“, „Feature“ oder „Dokumentation“) versehen. Dies half dabei, den Überblick über den Projektfortschritt zu behalten und die Anforderungen aus dem Lastenheft strukturiert abzuarbeiten. [@gitea_docs] [@gitea_issue_tracker]
Pull Requests
\
Um die Qualität des Codes zu sichern, wurden Änderungen nicht direkt in den Hauptzweig eingespielt, sondern über Pull Requests eingereicht. Ein Teammitglied konnte so die Änderungen eines anderen sichten, kommentieren und bei Bedarf Korrekturen anfordern. Erst nach einer erfolgreichen Überprüfung wurde der Code in den main-Branch gemergt. [@gitea_docs] [@gitea_pull_requests]
Actions
\
Gitea Actions wurden eingesetzt, um CI/CD-Pipelines (Continuous Integration / Continuous Deployment) zu realisieren. Bei jedem Push oder Pull Request wurden automatisierte Skripte ausgeführt, die das Projekt bauten. Dies reduzierte manuelle Fehlerquellen erheblich. Außerdem konnten wir mithilfe von CI/CD den Release Prozess einmalig festlegen und automatisieren, ohne bei jedem Update manuell den selben Prozess wiederholt durchgehen zu müssen. Das APT-Package Projekt enthält die CI/CD Konfiguration für das bauen von Oqtane, der Module und Themes, sowie das verpacken in ein APT Paket und dem veröffentlichen aller Pakete als eingenes Gitea Release. [Siehe Abschnitt \ref{sec:continuous-integration}] [@gitea_docs] [@gitea_actions]
Releases
\
Über die Release-Funktion wurden wichtige Meilensteine der Diplomarbeit festgeschrieben. Hierbei wird ein spezifischer Git-Tag mit einer Versionsnummer versehen und die dazugehörigen Binärdateien, Pakete und Dokumente archiviert. So lässt sich jederzeit auf einen stabilen, abgabebereiten Stand des Projekts zugreifen. [@gitea_docs]
Package Repositories
\
Gitea fungierte zusätzlich als Register für Pakete und Container-Images. Selbst erstellte Artefakte, wie das Debian Paket für die Bereitstellung der Anwendung, wurden direkt in der Gitea-Instanz versioniert gespeichert. Dadurch waren alle notwendigen Komponenten für das Deployment an einem zentralen Ort verfügbar und abrufbar. Gitea selbst unterstützt verschiedenste Pakettypen. Darunter fallen unteranderem NuGet- und Debianpakete. Für beide haben wir in dieser Arbeit verwendung gefunden. [@gitea_docs] [@gitea_packages]
Kommunikation
Module
Admin Modules
Eine C#-Solution, welche einige für den Admin-Einsatz geschriebene Module beinhaltet. Dieses Modul besteht aus drei Teilmodulen: einem Modul für den Versand von Rundmails, eines für die Einstellung der Token-Lebenszeit bei Tokens, welche per E-Mail verschickt werden, und eines, welches das Reporting-System übernimmt.
Mass Mailing
\
Das Mass-Mailer-Modul ist eine administrative Erweiterung für den AlumniHub, die es dem Vorstand ermöglicht, personalisierte Rundschreiben an alle registrierten Mitglieder zu versenden. Da die Pflege der Mitgliederdaten direkt im CMS erfolgt, bietet dieses Modul eine nahtlose Integration ohne den Export von CSV-Listen in externe Newsletter-Tools.
Integration von Brevo
\
Für den tatsächlichen Versand der E-Mails nutzen wir den Cloud-Dienst Brevo. Dieser bietet eine zuverlässige Zustellung (hohe Reputation der Mailserver), sowie die Möglichkeit die Zustell-, Öffnungs- und Klickraten zu beobachten. Das Limit von 300 E-Mails pro Tag stellt uns jedoch in der kostenlosen Variante vor eine Herausforderung.
Batch-Processing: Mails werden nicht sofort ("Fire and Forget") versendet, sondern in eine Versandwarteschlange geschrieben. Nachdem schon die Notifications-Infrastruktur, welche sich auch um den Mailversand kümmert, ins Framework eingebaut worden ist, wird diese gleich zum schedulen unserer E-Mails genutzt. Immer 100 Mails alle 24 Stunden, bis alle Ziele die Mails erhalten haben. Das Limit von 100 / Tag ist konservativ sehr niedrig angesetzt, damit Funktionen wie Passwort-Reset-Mails nicht (leicht) dadurch beeinflusst werden können.
Token Lifetime
\
Das Token-Lifetime-Modul wurde geschrieben, um die Token-Lebenszeit konfigurierbar zu machen. Notwendig war das, um die Passwort-Reset-Links im initialen Mailversand länger gültig sein zu lassen. Durch das Batch-Processing war es möglich, dass eine Mail erst Tage nach Erstellen des Links hinausgeschickt wird, und bei einer Standard-Ablaufdauer von zwei Tagen sind manche Links schon ungültig, bis sie den Mailserver erreichen. Ziel war es, die Änderung der Lebenszeit für Administratoren im User-Interface im Admin-Bereich möglich zu machen.
Technisch bedeutet das, dass die standardmäßig vorkonfigurierten DataProtectionTokenProviderOptions explizit konfiguriert werden müssen. [@andrewlock_token_lifetime] Der ASP.NET Core UserManager, welcher das generieren der Tokens übernimmt, verwendet einen DataProtectorTokenProvider und dieser wiederum kann mithilfe der DataProtectionTokenProviderOptions konfiguriert werden.
Es gibt zwei Möglichkeiten, wie man dieses Problem lösen kann:
der Workaround: Ein eigenes Modul, welches in seinerServer/Startup.csdie benötigten Werte korrekt setzt. Das hat den Vorteil, dass wir keine Änderungen im CMS selbst haben, sondern nur unsere eigene Erweiterung dafür schreiben. Andererseits könnte durch die undeterministische Ladereihenfolge einerace-conditionauftreten. [@race_conditions_pdf] Darüber hinaus besteht kein gleichzeitiger Zugriff auf die appsettings.json und den IServiceProvider, in dem die Konfiguration gesetzt werden muss. => Nachdem wir in der Konfigurationsphase auch noch keinen Datenbankzugriff haben, können die Werte nicht aus der Datenbank geladen werden, sondern wir müssen auf eine Textdatei zurückgreifen.die saubere Lösung: Eine Änderung im Kern von Oqtane. Also wird in unserem Fork von Oqtane die Konfigurationslogik für die Token-Lifetime implementiert. In diesem Fall könnte die Konfigurationslogik direkt inOqtaneServiceCollectionExtensions.cshinzugefügt werden, da hier auch alle anderen Oqtane-spezifischen Konfigurationen gesetzt werden. Das hat den Vorteil, dass der Code aufgeräumter und sauberer ist und dierace-conditionverhindert werden kann. [@race_conditions_pdf] Der initiale Grund dagegen ist, dass wir ein weiteres Git-Repository zu warten haben (den Fork vom Oqtane.Framework), welches jetzt nicht mehr mit Upstream commitgleich ist.
Der Workaround ist die Möglichkeit für die wir uns entschieden haben, allerdings ist das nicht die schönste Lösung. Eine eventuell nachfolgende Diplomarbeit kann an dieser Stelle ansetzen und die saubere Lösung implementieren.
Reporting System
\
Eine weitere Anforderung der Diplomarbeit war es Einträge in Modulen wie der Hall of Fame, dem Schwarzen Brett und dem Premium Bereich (Engineer Applications) melden zu können. Am Anfang war es wichtig, dass jeder schnell vorankommt, allerdings haben wir die Kommunikation Teamintern ein wenig verschlafen und dadurch ein paar Funktionen doppelt geschrieben. Dadurch kam es zu Inkonsistenzen in der Verwendung der unterschiedlichen Reporting Systeme. Deswegen haben wir uns am Ende für ein globales Reporting-System entschieden.
Angestrebt wurde folgender Ablauf für das Melden eines Eintrags:
%%| fig-cap: Reporting System: Ablaufdiagramm
sequenceDiagram
participant Module
actor User
participant ReportingComponent
participant ReportingHandler
User->>+Module: View: Entity
User->>+ReportingComponent: Report current Entity
rect rgba(0, 0, 0, 0)
ReportingComponent->>+User: Ask for reason
User->>-ReportingComponent: Enters Reason
end
ReportingComponent->>+ReportingHandler: New Report (includes entity and reason)
ReportingComponent->>-User: Done
Im oben dargestellten Ablaufdiagramm werden das Reporting-Component und der Reporting-Handler vom Reporting-System über Dependency Injection bereitgestellt, nicht vom Modul selbst. Dadurch erreichen wir eine bessere Trennung der Zuständigkeiten und halten das S in SOLID ein: Single responsibility. Die Module sind jetzt nicht mehr für das Reporting selbst verantwortlich, sondern müssen nur das Reporting-System einbinden.
Darüber hinaus erfüllt dieses Design das Open/Closed Principle: Das Reporting-System ist offen für Erweiterungen (neue Module können einfach andocken), aber geschlossen für Modifikationen (der Kern des Reporting-Systems muss nicht für jedes neue Modul geändert werden).
Damit DI funktioniert, muss für den DI-Consumer (also das Modul, welches das Reporting-System einbinden möchte) das Interface zur Kompilierzeit zur Verfügung stehen. Um das zu erreichen, habe ich eine neue Klassenbibliothek erstellt: Sie heißt Interfaces, wird per Gitea Actions automatisch in ein NuGet-Paket gebaut und in der Gitea Actions NuGet Registry veröffentlicht. Die Versionierung folgt dabei strikt den Git-Tags im Repository, was eine saubere Release-History ermöglicht. Dieses NuGet-Paket wird dann in jedem notwendigen Modul als Dependency hinzugefügt, und damit kann man modulübergreifend auf die Services und das IReportingComponent zugreifen.
Die Implementierung des IReportingComponents stellt nur eine Property (ReportType, welche den TypeName der Razor Komponente zurückliefert, damit DynamicComponent sie laden kann) und eine Methode (ConstructParameterList, welche das Parameter Dictionary erstellt. Nur zwecks Typensicherheit eingefügt) bereit. Mit dem DynamicComponent von Razor ist es möglich, per C# Code unterschiedliche Komponenten zu rendern und damit auch die per DI injizierte Klasse.
@inject IReportUI ReportUI
<DynamicComponent
Type="@ReportUI.ReportType"
Parameters="@ReportUI.ConstructParameterList(_item, RenderModeBoundary)"
/>
@code {
private IReportable _item;
}
Die Bereitstellung des Moduls geschieht im AdminModules Modul.
Event Registration
Dieses Modul ermöglicht es Administratoren und Absolventen, Veranstaltungen zu erstellen, auf der Seite zu veröffentlichen und zu verwalten, während Mitglieder ihre Teilnahme direkt über das Portal bestätigen oder absagen können. Dieses Modul dient der organisatorischen Unterstützung von Absolvententreffen und anderen Vereinsaktivitäten. Dieses Modul wurde gemeinsam mit Adam Gaiswinkler geschrieben, wobei er sich um die Darstellung der Events im Frontend gekümmert hat, während ich mich der Infrastruktur im Hintergrund angenommen habe.
Backend und Datenhaltung
\
Die serverseitige Implementierung basiert auf dem Repository-Pattern des Oqtane-Frameworks. Hierbei kommen zwei zentrale Repositories zum Einsatz:
Das EventRepository verwaltet die Metadaten der Veranstaltungen wie Name, Beschreibung, Datum und Ort.
Das ResponseRepository speichert die Rückmeldungen der Benutzer. Ein Eintrag verknüpft dabei die UserId mit der EventId und dem Status der Rückmeldung.
Die Kommunikation zwischen dem Client und dem Server erfolgt über einen REST-API-Controller EventRegistrationController, der sicherstellt, dass nur autorisierte Benutzer Änderungen vornehmen oder detaillierte Statistiken einsehen können.
Entity Relationship Diagram
\
erDiagram
direction LR
%%| filename: erd-event-registration
%%| fig-cap: ER Diagramm des Event Registration Moduls
EVENT ||--o{ RESPONSE : "has"
EVENT {
int EventId PK
int ModuleId
string Name
string Description
datetime EventDate
string Location
string CreatedBy
datetime CreatedOn
string ModifiedBy
datetime ModifiedOn
}
RESPONSE {
int EventResponseId PK
bool ResponseType
int OwnerId FK
int EventRegistrationId FK
int ModuleId
string CreatedBy
datetime CreatedOn
string ModifiedBy
datetime ModifiedOn
}
USER ||--o{ RESPONSE : "has"
USER {
int UserId PK
}
Statistik und Visualisierung
\
Ein wesentlicher Teil der administrativen Ansicht ist die Visualisierung der Anmeldezahlen. Hierfür wurde eine Integration von Chart.js realisiert, um den aktuellen Stand der Rückmeldungen grafisch aufzubereiten.
Um die Brücke zwischen dem C#-basierten Blazor-Frontend und der JavaScript-Bibliothek Chart.js zu schlagen, wurde ein dedizierter Interop-Service implementiert. Der JS-Interop-Layer ist zwar standardmäßig in Oqtane-Modulen vorgesehen, wurde jedoch in diesem Modul zum ersten Mal angepasst und produktiv eingesetzt. Der Ablauf der grafischen Darstellung gestaltet sich wie folgt:
- Datenaufbereitung: In der Edit-Komponente werden alle Rückmeldungen zu einem Event geladen und nach ihrem Typ, oder beliebigen anderen Merkmalen aggregiert.
- JS-Interop: Über die
CreateChart-Methode der Interop-Klasse wird die JavaScript-FunktioncreateChartin derModule.jsaufgerufen. Dabei werden die aggregierten Daten, Beschriftungen und Konfigurationsoptionen übergeben. - Canvas-Rendering: Die JavaScript-Logik erzeugt dynamisch ein HTML5-canvas-Element innerhalb eines Container-Divs und initialisiert daraufhin die Chart.js-Instanz, welche ein übersichtliches Pie-Chart mit den Registrierungsstatistiken rendert.
Durch diese Trennung bleibt die Geschäftslogik im C#-Code, während für die performante und ansprechende Darstellung auf etablierte Web-Technologien zurückgegriffen wird.
Schwarzes Brett
Das Modul "Schwarzes Brett" dient als digitale Anschlagtafel für den Absolventenverein. Mitglieder können hier Gesuche, Angebote oder allgemeine Informationen veröffentlichen. Es stellt eine zentrale Informationsdrehscheibe dar, die den informellen Austausch innerhalb des Vereins fördern soll.
Struktur und Anzeige
\
Die Anzeige der Einträge erfolgt in einer responsiven Grid-Ansicht (Index-Komponente), wobei jeder Eintrag als Karte (Card) dargestellt wird. Dieses Design sorgt für eine übersichtliche Präsentation auch bei einer größeren Anzahl von Mitteilungen.
Bilderunterstützung: Das Modul nutzt die Oqtane-interne Dateiverwaltung. Wenn ein Bild für einen Eintrag hochgeladen wurde, wird dieses über einen Image-Proxy skaliert und als Vorschaubild angezeigt. Fehlt ein Bild, wird ein konsistenter Platzhalter verwendet, um das visuelle Gleichgewicht der Grid-Ansicht zu wahren.
Detailansicht: Die Details-Komponente bietet eine fokussierte Ansicht des Eintrags mit vollständiger HTML-Beschreibung. Für die Eingabe und Formatierung der Texte wird auf die von Oqtane standardmäßig bereitgestellten Rich-Text-Editoren zurückgegriffen, was eine konsistente Nutzerführung im gesamten CMS gewährleistet. Ergänzt wird dies durch Metadaten wie Erstellungsdatum und Autor.
Automatisierter E-Mail-Digest
\
Um die Mitglieder regelmäßig über neue Inhalte zu informieren, wurde ein automatisierter Cronjob implementiert. Dieser Job läuft im Hintergrund des Oqtane-Frameworks und führt folgende Schritte aus:
- Filterung: Der Job identifiziert alle Einträge, die seit dem letzten Versand erstellt wurden.
- Zielgruppenselektion: Es werden alle Benutzer identifiziert, die der Rolle "Absolventen" angehören.
- Zusammenstellung: Für jeden dieser Benutzer wird eine personifizierte E-Mail-Notification generiert, welche eine Zusammenfassung der neuen Einträge enthält.
- Versand: Die generierten Notifications werden in die Warteschlange der Notification-Infrastruktur eingereiht und sukzessive versendet.
Integration des Reporting-Systems
\
Ein wichtiges Merkmal des Schwarzen Bretts zur Sicherstellung der Inhaltsqualität ist die Anbindung an das globale Reporting-System (siehe 5.4). In der Detailansicht wird über Dependency Injection die IReportUI-Komponente eingebunden. Mithilfe der DynamicComponent von Blazor wird die Melde-Funktion nahtlos in die Oberfläche des Moduls integriert. Dadurch können unangemessene Inhalte direkt von Benutzern gemeldet werden.
Technischer Hintergrund
\
Auf der Serverseite folgt das Modul dem etablierten Muster mit einem BlackBoardRepository für den effizienten Datenbankzugriff und einem BlackBoardController für die API-Bereitstellung. Die Implementierung des Scheduled Jobs als HostedServiceBase ermöglicht eine tiefe Integration in die Oqtane-Infrastruktur bei gleichzeitig geringem Ressourcenverbrauch.
Learnings
Produktion != Staging
Ein Learning, welches doch relativ schnell aufkam, ist im Bereich der IT eigentlich kein unbekanntes. Wir hatten dieses Learning relativ bald, im Frühling 2025, als die ersten Probleme mit dem Deployment von Oqtane aufkamen. Oqtane war zwar in unserer Entwicklungsumgebung sehr einfach einzurichten gewesen, das Deployment in der Cloud von Hetzner war jedoch geplagt von Problemen. Im Zeitraum von Mai bis Oktober hatten wir keine laufende Produktivumgebung. Dadurch sind wir mit dieser Diplomarbeit auch in Zeitverzug gekommen. Hätte ich mich vor dem Start der Diplomarbeit mit dem Deployment von Oqtane auseinandergesetzt, dann wäre das in Fehlende Dokumentation beschriebene Problem früher aufgekommen und der Zeitverzug wäre nicht so groß oder noch ganz vermeidbar gewesen.
Teamleitung (Motivation / Downsizing)
Nachdem ich mich von Anfang an vollkommen in das Deploymentproblem von Oqtane gestürzt habe, habe ich meine Rolle als Teamleitung etwas schleifen lassen. Dadurch fehlte bei einigen Teammitgliedern initial die Identifikation mit dem Projekt und in weiterer Folge auch die Motivation, an diesem Projekt mitzuarbeiten. Nachdem im Verlauf des Frühlings und über den Sommer von der Hälfte des Teams trotz Besprechungen und Mahnungen keine Beiträge zu dem Projekt kamen, haben Hr. Prof. Gürth und ich uns dazu entschieden, uns von zwei Personen vor dem Unterschreiben des Projektantrages zu trennen. Grund dazu war die Angst, die mangelnde Motivation zieht das restliche Team mit hinunter. Wir wollten uns trotz des Downsizings nicht an Funktionalitäten sparen und haben uns für das nächste halbe bis dreiviertel Jahr einen ziemlich strikten Zeitplan vorgenommen.
Arbeitszeiteinschätzung (Zeitverzug)
Ein wesentliches Learning aus dem Projektverlauf war die Diskrepanz zwischen der initialen Planung und dem tatsächlichen Aufwand. Ursprünglich wurde der Zeitaufwand für das Aufsetzen der Infrastruktur und die Einarbeitung in das Oqtane-Framework auf etwa drei Wochen geschätzt. In der Realität nahm dieser Prozess jedoch mehrere Monate in Anspruch.
Es gibt mehrere Gründe dafür:
Fehlende Dokumentation
\
Fehlende oder nur schlechte Dokumentation von Oqtane: Einige Probleme im Deployment wurden in langer und mühseliger Arbeit auseinandergebrochen und in weitere immer kleinere Probleme unterteilt. Dadurch, dass wir alle keine Erfahrung mit der Entwicklung und dem Deployment von ASP.NET-Core-Anwendungen hatten und die Dokumentation doch schlecht war, blieb uns manchmal nichts anderes übrig, als mit WireShark den Netzwerktraffic mitzuschneiden und nebenbei im Git-Repository die geloggten Codezeilen zu finden und so das Framework von innen heraus kennenzulernen. Dadurch hatte ich dann nach einer Einarbeitungszeit von vier Monaten ziemlich jede Stelle im Sourcecode von Oqtane gesehen und finde mich umso schneller zurecht.Team-Konsolidierung: Durch das notwendige Downsizing des Teams mussten Aufgaben neu verteilt werden, was die individuelle Arbeitslast erhöhte und die Konzentration auf die Kernentwicklung zeitweise verzögert hat.Abhängigkeit von der Infrastruktur: Dadurch, dass wir bis in den Oktober / November hinein nicht wussten, ob wir weiter bei Oqtane bleiben können, haben die anderen Teammitglieder nicht mit der sinnvollen Entwicklung ihrer Module starten können. Zitat: "Und was wenn wir am Ende doch noch das CMS umstellen müssen?" => Auch wenn der Auftrag war, mit der Modulentwicklung zu starten war die Motivation meiner Teammitglieder nicht so hoch. Selbst wenn sie nicht direkt von der Infrastruktur mit der Ausführung ihrer Aufgaben abhängig waren, motiviert waren sie wegen der Umstände auch nicht.
Reaktion auf den Verzug: Um das Projektziel dennoch zu erreichen, wurde der Zeitplan im Herbst 2025 massiv gestrafft. Durch die Umstellung auf einen strikteren 14-tägigen Sprint-Rhythmus und die Priorisierung von Core-Funktionalitäten (MVP-Ansatz) konnte der Rückstand teilweise aufgeholt werden.
Fazit: Die Erfahrung zeigt, dass gerade bei "Nischen-Frameworks" wie Oqtane ein deutlich höherer Puffer für die Einarbeitungs- und Infrastrukturphase (Faktor 2 bis 3 der ursprünglichen Schätzung) eingeplant werden muss.
90% fertig, oder fertig?: Es gibt einige "Regeln", wie: das Paretoprinzip, Hofstadter’s Law und die 90-90 Regel. Letztere wurde im Jahr 1985 von Jon Bentley in einer Kolumne "Programming pearls" veröffentlicht. Ausgeschrieben lautet sie:
[Rule of Credibility] The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time. [@bentley1985programming] Diese Diplomarbeit liefert weitere Evidenz, dass diese Faustregel stimmt.
Sprints und Meetings (in Zukunft ja asynchron)
Ein zentrales Problem in unserer ursprünglichen Arbeitsweise war die Kopplung von Besprechungsterminen mit festen „Commit-Deadlines“ (dem Ende des aktuellen Sprint zyklusses). Da wir uns einmal pro Woche für sechs Stunden am Stück trafen, entstand ein destruktives Muster:
- Der "Last-Minute-Commit"-Druck: In den Stunden unmittelbar vor dem Meeting wurden Aufgaben unter Zeitdruck abgeschlossen, um im Meeting Fortschritte präsentieren zu können. Dies führte dazu, dass unfertiger oder unzureichend getesteter Code („Quick and Dirty“) in das Repository gepusht wurde.
- Fehlende Review-Kultur: Da die Commits erst kurz vor dem Meeting eintrafen, blieb dem restlichen Team keine Zeit für fundierte Code-Reviews. Die Besprechungszeit wurde somit für die Fehlersuche statt für strategische Planung genutzt.
- Ineffizienz: Lange Präsenz-Meetings blockierten wertvolle Entwicklungszeit, ohne die technische Qualität zu steigern.
Lösungsansatz: Meetings und Besprechungen asynchron zueinander setzen.
- Asynchrone Daily-Updates: Statusberichte erfolgen schriftlich (z. B. in Gitea Issues oder YouTrack), nicht mehr in stundenlangen Call-Marathons. Das nimmt den zeitlichen Druck vom einzelnen Entwickler. Oder zumindest in kurzen Commitnachrichten, welche am Ende des Tages automatisch an alle Teammitglieder zum Überblick gesendet werden (eventuell mit @username-Tagging, um eine Person nochmal genau anzusprechen).
- Review-First-Policy: Ein Feature gilt erst dann als „fertig“, wenn es einen asynchronen Code-Review-Prozess durchlaufen hat. Das Meeting dient nur noch der Klärung von Blockern, nicht der Präsentation von Code. Das war eigentlich schon von Anfang an in unserer
Definition of Donefestgelegt worden. - Entkoppelung von Meeting und Deadline: Meetings sollten der Synchronisation dienen, während die Abgabe von Arbeitspaketen kontinuierlich (Continuous Integration) erfolgen muss, um Lastspitzen (in der Gitea Actions Pipeline) am Tag der Besprechung zu vermeiden.
Fazit
Die Entwicklung des AlumniHubs auf Basis von Blazor und Oqtane war eine Reise geprägt von technischer Tiefe, organisatorischen Herausforderungen und wertvollen Erkenntnissen über moderne Software-Architektur. Rückblickend lässt sich festhalten, dass die Wahl des .NET-Stacks sowohl die erwarteten Stärken als auch unerwartete Hürden mit sich brachte.
Technisch gesehen haben sich die statische Typisierung von C# und das integrierte Dependency Injection Framework als äußerst wertvolle Werkzeuge erwiesen. Sie haben die Entwicklung komplexer, modularer Strukturen – wie des generischen Reporting Systems – erheblich vereinfacht und für eine hohe Konsistenz gesorgt. Dennoch zeigt der Vergleich zu anderen Stacks wie React und Go, dass Architektur-Vorteile nicht exklusiv an ein einzelnes Ökosystem gebunden sein müssen. Die Erfahrung mit Protobufs in anderen Kontexten verdeutlicht, dass eine sprachenagnostische Typisierung oft flexibler ist, ohne die Bindung an ein spezifisches Framework-Ökosystem zu erzwingen. .NET bleibt jedoch eine absolut valide und stabile Wahl für Projekte dieser Größenordnung.
Ein zentraler Aspekt der Reflexion betrifft den Entwicklungsprozess. Die Erkenntnis, dass eine strikte Review-Kultur und eine frühzeitige CI/CD-Automatisierung essenziell sind, kam erst im Verlauf des Projekts voll zum Tragen. Initial fehlende Strukturen führten dazu, dass Module oft unreflektiert hochgeladen wurden, was die Qualitätssicherung erschwerte. Für zukünftige Projekte wäre ein „Automation-First“-Ansatz die oberste Priorität.
In organisatorischer Hinsicht war die Verkleinerung des Teams ein Wendepunkt. Trotz der gestiegenen individuellen Arbeitslast führte das „Downsizing“ zu einer signifikant höheren Identifikation mit dem Projekt und einer gesteigerten Effizienz. Die klarere Verantwortung jedes einzelnen Mitglieds sorgte für eine Dynamik, die im größeren, weniger fokussierten Team nicht erreicht werden konnte.
Persönlich bleibt vor allem die fünfmonatige Auseinandersetzung mit der Deployment-Problematik im Gedächtnis. Dass die Lösung für eine komplexe Infrastruktur-Hürde schließlich in einem unerwarteten Moment außerhalb der Arbeitsumgebung – fast schon ironisch – auftauchte, unterstreicht eine wichtige Lektion für jeden Entwickler: Beharrlichkeit ist notwendig, aber oft braucht es auch den nötigen Abstand, um den entscheidenden Blickwinkel zu finden.
Abschließend lässt sich sagen, dass der AlumniHub nicht nur eine funktionsfähige Plattform für den Absolventenverein darstellt, sondern für mich persönlich als Beweis für die Bedeutung von technischer Neugier, architektonischer Weitsicht und der Fähigkeit zur kritischen Selbstreflexion dient. (Mehr dazu unter Learnings)








