Architektur

So ist diese Website gebaut.

Ein modularer Monolith mit .NET 10 und Blazor, betrieben auf Azure App Service mit Azure SQL – beschrieben als Infrastruktur-Code und automatisch ausgeliefert. Klein genug für eine Bewerbungsseite, aufgebaut wie ein echtes Produkt.

01Azure

Die Infrastruktur

Alle Ressourcen liegen in einer Ressourcengruppe und werden per Bicep angelegt. Die App verbindet sich passwortlos per Managed Identity mit der Datenbank.

Azure-Architektur von KenoKoch.de Der Browser ruft kenokoch.de über HTTPS auf. Azure App Service (Linux, .NET 10) liefert statisches HTML, hält SignalR-Verbindungen und stellt die WebAssembly-Dateien bereit. Die Anwendung verbindet sich per Managed Identity mit Azure SQL Database (Serverless) und sendet Telemetrie an Application Insights. GitHub Actions baut, testet und deployt per OpenID Connect; die Infrastruktur wird mit Bicep beschrieben. Azure · Ressourcengruppe rg-kenokoch · Region West Europe Browser HTML · SignalR .NET WebAssembly GitHub Actions Build · Test · Publish · Deploy (OIDC) Bicep Infrastruktur als Code Azure App Service Linux · .NET 10 · HTTPS · Managed Certificate Static SSR HTML pro Request Blazor Server SignalR · WebSockets Minimal APIs OpenAPI · Health 4 Module modularer Monolith Azure SQL Serverless · Free Offer [guestbook] [analytics] Application Insights Log Analytics · OpenTelemetry HTTPS / WSS Managed Identity Telemetrie kenokoch.de DNS → App Service

Azure App Service (Linux)

Hostet die .NET-10-Anwendung mit WebSockets für Blazor Server, Always On und Health-Check-Pfad. HTTPS mit kostenlosem, verwaltetem Zertifikat für die eigene Domain.

Azure SQL Database

Serverless im Rahmen des kostenlosen Angebots: pausiert bei Inaktivität und wacht beim ersten Zugriff auf. Die Anwendung ist darauf ausgelegt und bleibt währenddessen bedienbar.

Managed Identity

Kein Passwort in Konfiguration oder Pipeline: App Service authentifiziert sich mit seiner Identität bei Microsoft Entra ID und damit bei Azure SQL.

Application Insights

Requests, Abhängigkeiten, Fehler und Logs per OpenTelemetry – mit Log Analytics als Speicher.

02Request-Lebenszyklus

Was passiert, wenn Sie diese Seite öffnen?

  1. 1

    DNS und TLS

    kenokoch.de zeigt auf Azure App Service. Die Verbindung ist per HTTPS gesichert, www wird auf die Hauptdomain umgeleitet.

  2. 2

    Statisches Server-Rendering

    Kestrel rendert die Seite als HTML – schnell und für Suchmaschinen lesbar. Security-Header inklusive Content-Security-Policy mit Nonce kommen mit.

  3. 3

    Interaktive Inseln

    Nur die Teile, die interaktiv sein müssen, bauen eine SignalR-Verbindung auf (Live-Zähler, SPS, Gästebuch). Der Rest bleibt reines HTML.

  4. 4

    WebAssembly bei Bedarf

    Auf der Demo-Seite lädt der Browser die .NET-Runtime nach und führt C# lokal aus – ohne Server-Roundtrips.

  5. 5

    Datenzugriff

    EF Core spricht über Managed Identity mit Azure SQL. Statistik wird gepuffert und gebündelt geschrieben, damit kein Request auf die Datenbank wartet.

  6. 6

    Beobachtbarkeit

    Jeder Request landet als Telemetrie in Application Insights; der Server-Timing-Header zeigt die Renderzeit im Browser.

03Modularer Monolith

Ein Deployment, klare Grenzen

Jedes Modul ist ein eigenes Projekt mit Domäne, Persistenz, API und UI. Der Host setzt sie zusammen – so wie die Unternehmensplattform, an der ich arbeite.

Aufbau des modularen Monolithen Der Host referenziert vier Module. Module referenzieren sich nicht gegenseitig; das Modul Besucher und Echtzeit kennt nur die Contracts von Gästebuch und SPS-Simulator. Alle Module bauen auf den Bausteinen BuildingBlocks und UI auf. KenoKoch.Web · Host Komposition · Seiten · Layout · OpenAPI · Health · Security Web.Client WebAssembly Profil Lebenslauf & Skills /api/profile Gästebuch EF Core · Validierung schema [guestbook] Besucher & Echtzeit SignalR · Statistik schema [analytics] SPS-Simulator Soft-SPS · HMI OB1 · 100 ms Guestbook.Contracts GuestbookEntrySubmitted Plc.Contracts PlcStarted · PlcAlarmRaised nur über Contracts BuildingBlocks IModule · Event-Bus · Persistenz · Health Checks UI Icons · Code-Blöcke · Formatierung (Server + WASM) von allen Modulen genutzt
GuestbookModule.cs – ein komplettes Modul C#
using KenoKoch.BuildingBlocks.Modules;
using KenoKoch.BuildingBlocks.Persistence;
using KenoKoch.Modules.Guestbook.Application;
using KenoKoch.Modules.Guestbook.Contracts;
using KenoKoch.Modules.Guestbook.Persistence;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace KenoKoch.Modules.Guestbook;

/// <summary>Modul "Gästebuch": Besucher hinterlassen Grüße, gespeichert mit EF Core im Schema [guestbook].</summary>
public sealed class GuestbookModule : IModule
{
    public string Name => "Guestbook";

    public string DisplayName => "Gästebuch";

    public string Description => "Einträge von Besuchern: Validierung, Spam-Schutz und Persistenz mit EF Core im eigenen Schema.";

    public string? DatabaseSchema => GuestbookDbContext.SchemaName;

    public IReadOnlyCollection<Type> PublishedEvents => [typeof(GuestbookEntrySubmitted)];

    public void AddServices(IServiceCollection services, IConfiguration configuration)
    {
        services.AddOptions<GuestbookOptions>().Bind(configuration.GetSection(GuestbookOptions.SectionName));
        services.AddModuleDbContext<GuestbookDbContext>(configuration, GuestbookDbContext.SchemaName);
        services.AddSingleton<SubmissionThrottle>();
        services.AddScoped<GuestbookService>();
    }

    public void MapEndpoints(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/guestbook", GetEntriesAsync)
            .WithName("GetGuestbookEntries")
            .WithSummary("Freigegebene Gästebucheinträge")
            .WithDescription("Liefert die neuesten freigegebenen Einträge aus Azure SQL (Parameter take: 1–50).");
    }

    private static async Task<Results<Ok<GuestbookApiResponse>, ProblemHttpResult>> GetEntriesAsync(
        GuestbookService service,
        DatabaseState database,
        int? take,
        CancellationToken cancellationToken)
    {
        if (!database.IsReady)
        {
            return TypedResults.Problem(
                title: "Datenbank derzeit nicht verfügbar",
                detail: database.Message,
                statusCode: StatusCodes.Status503ServiceUnavailable);
        }

        var page = await service.GetLatestAsync(take ?? 10, cancellationToken);
        var entries = page.Entries
            .Select(e => new GuestbookApiEntry(e.Name, e.Company, e.RoleLabel, e.Message, e.CreatedAt))
            .ToArray();

        return TypedResults.Ok(new GuestbookApiResponse(page.TotalCount, entries, Math.Round(page.Query.Duration.TotalMilliseconds, 2)));
    }
}
04Entscheidungen

Warum so – und nicht anders?

Architektur ist eine Abfolge von Abwägungen. Hier sind die wichtigsten, kurz begründet.

ADR-001

Blazor statt JavaScript-Framework

Eine Sprache, ein Typsystem und geteilter Code von der Datenbank bis zur Oberfläche. Für Business-Anwendungen im .NET-Umfeld spart das Kontextwechsel und doppelte Modelle.

Abwägung: Das Ökosystem an UI-Komponenten ist kleiner als bei React – für Formulare, Tabellen und Dashboards aber mehr als ausreichend.

ADR-002

Statisches SSR mit interaktiven Inseln

Seiten werden als HTML ausgeliefert und laden schnell. Interaktivität gibt es nur dort, wo sie gebraucht wird – und dann gezielt als Server- oder WebAssembly-Komponente.

Abwägung: Mehr Nachdenken über Render-Modi pro Komponente, dafür deutlich weniger Last und bessere Ladezeiten.

ADR-003

Modularer Monolith statt Microservices

Ein Deployment, eine Datenbank, einfache Transaktionen – aber harte Grenzen zwischen Fachlichkeiten. Einzelne Module ließen sich später herauslösen, wenn es wirklich nötig wird.

Abwägung: Grenzen müssen aktiv geschützt werden – hier durch Architekturtests im CI.

ADR-004

Schema pro Modul

Jedes Modul besitzt seine Tabellen und Migrationen. Kein Modul liest fremde Tabellen; Daten fließen über Events.

Abwägung: Abfragen über Modulgrenzen hinweg brauchen eine API oder ein Read-Model statt eines einfachen JOINs.

ADR-005

Azure SQL Serverless

Für eine Seite mit wenig, aber unregelmäßigem Traffic ideal: Abrechnung nach Nutzung bzw. im kostenlosen Kontingent, automatische Pause bei Inaktivität.

Abwägung: Kaltstart nach der Pause – die Anwendung zeigt das transparent an und blockiert nie den Seitenaufbau.

ADR-006

Passwortlos mit Managed Identity

Keine Geheimnisse in appsettings, Pipeline oder Key Vault für die Datenbank. Rechte werden über Microsoft Entra ID vergeben und sind nachvollziehbar.

Abwägung: Einmaliges Setup des Datenbankbenutzers per T-SQL nach dem ersten Deployment.

05Qualität & Betrieb

Vom Commit bis in die Cloud

  1. Push auf main oder Pull Request
  2. Build .NET 10 · Release · Warnungen = Fehler
  3. Tests Unit- und Architekturtests mit xUnit
  4. Publish Artefakt inkl. komprimierter Assets
  5. Deploy OIDC-Login, kein Secret im Repository
  6. Smoke-Test /health nach dem Deployment
Content-Security-Policy Skripte nur vom eigenen Origin, Nonce für die Import-Map, keine Frames.
Rate Limiting Fixed-Window-Limits pro IP für alle API-Endpunkte und das Gästebuch.
Health Checks Liveness ohne Datenbank, Readiness mit Migrationsstand pro Modul.
Resilienz Retry-Strategie für transiente Azure-SQL-Fehler, Migration im Hintergrund.
Central Package Management Alle NuGet-Versionen an einer Stelle, transitive Sicherheitsupdates gepinnt.
Barrierefreiheit Semantisches HTML, Tastaturbedienung, Tabellen als Alternative zu Diagrammen, reduzierte Bewegung.
Da ist etwas schiefgelaufen. Seite neu laden

Verbindung zum Server wird wiederhergestellt …

Noch keine Verbindung – neuer Versuch in Sekunden.

Die Verbindung konnte nicht wiederhergestellt werden.
Bitte erneut versuchen oder die Seite neu laden.

Die Sitzung wurde vom Server pausiert.

Die Sitzung konnte nicht fortgesetzt werden.
Bitte erneut versuchen oder die Seite neu laden.