Files
pm/Diplomarbeitsbuch-individueller-teil-Konstantin-Hintermayer.md
Konstantin Hintermayer 8f30e063ce
All checks were successful
Gemini Writing Review / gemini-review (pull_request) Successful in 1m18s
Word Count / count-words (pull_request) Successful in 34s
Fix: Typos
2026-03-02 09:50:01 +00:00

7.9 KiB

Table of Contents

Technologie

Entwicklung mit ASP.NET (Was ist Blazor? / Was ist Razor? / Kestrel)

Was ist Oqtane? Architektur von Oqtane?

Oqtane ist ein Framework und CMS zur Entwicklung von Webseiten mithilfe von ASP.NET und Blazor. 1 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. 2

Ein Module (Modul) soll neue Funktionalitäten in das CMS hinzufügen und ein Theme soll die ganze Gestaltung der Website (die Shell) festlegen. 2

Architektur eines Moduls

Ein Modul in Oqtane besteht aus 4 Projekten. Server, Client, Shared und Package.

Im Server-Projekt liegt Sourcecode, welcher serverseitig ausgeführt werden soll. In der Praxis bedeutet das: alle Repositories, Controller, Manager, Migrationen und Server-Services (entwickelt nach einem Interface definiert im Client) und Server-Startuplogik.

Im Client-Projekt liegen Code und Razor-Komponenten für den Client. Also Client-Staruplogik, Client-Services (+ Inferfaces dafür, die Services hier sollen lediglich die Server-Services über HTTP aufrufen), 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 3 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 (DB Migrationen werden gemacht und die Pakete entpackt).

Systemarchitektur (Postgres / Oqtane / Nginx )

architecture-beta
    group server(server)[Server]

    service db(database)[PostgreSQL] in server
    service oqtane(server)[Oqtane] in server
    service nginx(server)[NginX] in server
    
    service internet(cloud)[Internet] 

    internet:R <--> L:nginx
    nginx:R <--> L:oqtane
    oqtane:R <--> L:db

Websockets und HTTP 1.1

Dependency injection

Dependency Inversion Principle 3

Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines von den 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. 4
  • Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die Low-Level-Implementierung sollen gemäß der Abstraktionsschicht implementiert werden. 4

Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph-Modell.

architecture-beta
    service a(mdi:package-variant-closed)[Klasse A] 
    service b(mdi:package-variant-closed)[Klasse B]
    service c(mdi:package-variant-closed)[Klasse C]

    a:R --> L:b
    b:R --> L:c

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
    service a(mdi:package-variant-closed)[Klasse A] 
    service b(mdi:package-variant-closed)[Klasse B]
    service ib(mdi:car-clutch)[Interface B]
    service c(mdi:package-variant-closed)[Klasse C]
    service ic(mdi:car-clutch)[Interface C]

    a:B --> T:ib
    ib:R <-- L:b
    b:B --> T:ic
    ic:R <-- L:c

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 Injektion ist in .NET genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. 5

Alle Dependencies werden in einem Service-Container zur Verwaltung registriert. .NET hat einen eingebauten Service-Container (eine Implementierung des IServiceProvider). 5

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 dannach bereitgestellt. 5

Hier ein Beispiel aus der Dokumentation von Microsoft: 5

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, dannach 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.

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

Projektmanagement

Scrum

YouTrack

Gitea (mit Issues)

Git

Kommunikation

Learnings

Probleme mit Oqtane

Arbeitszeiteinschätzung (Zeitverzug)

Teamleitung (Motivation / Downsizing)

Produktion != Staging

Sprints und Meetings (in Zukunft ja asynchron)

Modules

Mass Mailer

Event Registration

Schwarzes Brett