Files
SlipItIn/openwiki/architecture/overview.md
Tim Krampitz de852aada7
Some checks failed
Build / build (pull_request) Has been cancelled
Update OpenWiki documentation and claims
Reference AGENTS.md in CLAUDE.md and record verification claims for
the architecture overview and operations runbook.

Signed-off-by: Tim Krampitz <Tim.Krampitz@live.com>
2026-09-07 17:53:25 +02:00

17 KiB

type, title, description, tags, openwiki, verified
type title description tags openwiki verified
Architecture System Architecture & Security Overview Technical architecture of SlipItIn, covering .NET Aspire orchestration, ASP.NET Core SignalR hub, .NET MAUI MVVM client layer, WeakReferenceMessenger event routing, dual-layer local storage, and concurrency safety via IDbContextFactory.
architecture
aspire
signalr
jwt
efcore
security
maui
mvvm
concurrency
privacy
roles change_kinds source_paths symbols validation_commands
architecture
domain
lifecycle
public-api
SlipItIn/MauiProgram.cs
SlipItIn/Services/SignalRService.cs
SlipItIn/Services/GameStateService.cs
SlipItIn.Server/Program.cs
SlipItIn.Server/Hubs/GameHub.cs
SignalRService
GameStateService
LocalStorageService
GameHub
GameService
IDbContextFactory
dotnet build SlipItIn.slnx
by at
openwiki/0.5.0 2026-09-07T15:04:46.288Z

System Architecture & Security Overview

SlipItIn is designed as a distributed, real-time application using modern .NET 10 architecture. This document details the orchestration model, backend server structure, .NET MAUI MVVM client architecture, real-time connection resilience, messaging infrastructure, security model, and concurrency safeguards.

The architectural foundation orchestrates services with .NET Aspire, while enforcing security & privacy on domain entities and serving real-time hub endpoints for all active game sessions. Source code structure for all architectural components can be found in the Source Code Map.


1. .NET Aspire Orchestration Model

The solution uses .NET Aspire to orchestrate application resources, services, and infrastructure dependencies:

  • AppHost (SlipItIn.AppHost/AppHost.cs):
    • Provisions a PostgreSQL database container (AddPostgres("pgsql").AddDatabase("postgresdb")).
    • Registers the backend project SlipItIn.Server with a direct reference to the PostgreSQL resource, plus WaitFor(db) to ensure the database is ready before the server starts.
    • Registers the client project SlipItIn with service discovery references to SlipItIn.Server via WithReference(server).
  • Service Defaults (SlipItIn.ServiceDefaults/Extensions.cs):
    • Configures OpenTelemetry logging (with IncludeFormattedMessage and IncludeScopes), metrics (AspNetCore, HttpClient, Runtime), and tracing.
    • Exposes standardized health check endpoints (/health and /alive), filtering health paths from tracing.
    • Enforces automatic HTTP resilience (AddStandardResilienceHandler()) and service discovery.

2. ASP.NET Core Backend Architecture (SlipItIn.Server)

The backend is an ASP.NET Core Web API & SignalR application providing REST endpoints for user management and real-time WebSockets for game state synchronization.

Key Backend Components

  1. Program.cs:
    • Registers AddServiceDefaults(), AddNpgsqlDataSource("postgresdb"), and AddDbContextFactory<SlipItInDbContext>().
    • Configures JWT Bearer authentication with custom OnMessageReceived token resolution for SignalR and OnTokenValidated for user existence checks.
    • Adds SignalR, CORS (AllowAll), controllers, and OpenAPI.
    • Automatically executes database migrations (db.Database.Migrate()) and seeds test data (DbSeeder.SeedAsync()) at startup.
  2. AuthController.cs:
    • Manages user registration (/api/auth/register), authentication (/api/auth/login), and profile retrieval (/api/auth/me).
    • Uses BCrypt.Net for secure password hashing.
    • Issues JWT tokens signed with Jwt:Key, containing ClaimTypes.NameIdentifier, ClaimTypes.Name, and ClaimTypes.Email.
    • Uses IDbContextFactory for thread-safe database access per request.
  3. GameHub.cs:
    • Real-time SignalR hub mapped to /hubs/game.
    • Annotated with [Authorize] to reject unauthenticated WebSocket connections.
    • Extracts and verifies user claims, delegating state mutations to IGameService.
    • Manages ConnectionId lifecycle: stores it on OnConnectedAsync (auto-rejoining the lobby group), and clears it on OnDisconnectedAsync.
  4. GameService.cs:
    • Implements core business logic: game creation, player joins, card dealing, slip submission, challenge creation, and resolution.
    • Adds hostPlayer directly to game.Players before context.Games.Add(game) to ensure EF Core navigation tracking consistency.

3. JWT Authentication & SignalR Security

In real-time SignalR applications, clients can attempt to spoof player identifiers. SlipItIn eliminates this vulnerability through strict claim validation and authorization checks.

SignalR Token Extraction

Since WebSockets cannot pass custom HTTP headers during connection handshakes, Program.cs configures JWT query parameter extraction:

options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var accessToken = context.Request.Query["access_token"];
        var path = context.HttpContext.Request.Path;
        if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
        {
            context.Token = accessToken;
        }
        return Task.CompletedTask;
    }
};

Token Validation

OnTokenValidated extracts the NameIdentifier claim, creates a short-lived DbContext via IDbContextFactory, and verifies the user still exists and has IsActive == true. If the user is missing or inactive, the token is rejected before any hub method runs.

Claims Validation & Access Control in GameHub.cs

Inside GameHub.cs, claims are extracted directly from the authenticated SignalR context:

  • GetAuthenticatedUserId(): Extracts Context.User?.FindFirst(ClaimTypes.NameIdentifier). Throws UnauthorizedAccessException if missing or invalid.
  • ValidatePlayerAccessAsync(playerId, authUserId): Queries the database to verify that Player.UserId matches the authenticated user ID.
  • ValidateHostAccessAsync(gameId, authUserId): Verifies that Game.HostId matches the authenticated user ID before allowing lobby configuration or game start.
  • ValidateUserInGameAsync(gameId, authUserId): Verifies the authenticated user is a participant in the specified game, used by RequestGameState.
  • ValidateChallengeTargetAccessAsync(challengeId, authUserId): Ensures only the accused player can resolve a challenge against them.

Real-Time Auth Flow Diagram

sequenceDiagram
    autonumber
    actor Client as MAUI Client
    participant Auth as AuthController
    participant Hub as GameHub
    participant Service as GameService
    participant DB as SlipItInDbContext

    Client->>Auth: POST /api/auth/login
    Auth->>DB: Query User and Verify BCrypt Hash
    Auth-->>Client: 200 OK with JWT Access Token
    Client->>Hub: Connect WebSocket /hubs/game?access_token=JWT
    Hub->>Hub: Validate JWT Signature, Claims, and User Active Status
    Client->>Hub: JoinLobby with lobbyCode
    Hub->>Hub: GetAuthenticatedUserId
    Hub->>Service: JoinGameAsync with lobbyCode, userId, connectionId
    Hub-->>Client: Broadcast PlayerJoined with GameStateDto

Sequence diagram showing user authentication via REST and subsequent authenticated SignalR WebSocket connection.


4. .NET MAUI Client Architecture (SlipItIn)

The cross-platform client app uses the MVVM Pattern built on CommunityToolkit.Mvvm and Dependency Injection configured in MauiProgram.cs.

graph TD
    subgraph UI_Layer["UI Layer - Views"]
        LoginPage["LoginPage"]
        RegisterPage["RegisterPage"]
        LobbyPage["LobbyPage"]
        GameBoardPage["GameBoardPage"]
    end

    subgraph ViewModel_Layer["ViewModel Layer"]
        LoginVM["LoginViewModel"]
        RegisterVM["RegisterViewModel"]
        LobbyVM["LobbyViewModel"]
        GameBoardVM["GameBoardViewModel"]
    end

    subgraph Messaging_Layer["Messaging Layer"]
        Messenger["WeakReferenceMessenger"]
        Messages["Messages including GameStateChanged, PlayerHandChanged, ChallengeReceived, SyncStateChanged"]
    end

    subgraph Service_Layer["Service Layer"]
        GameStateSvc["GameStateService"]
        SignalRSvc["SignalRService"]
        AuthSessionSvc["AuthSessionService"]
        ApiSvc["ApiService"]
        LocalStorageSvc["LocalStorageService"]
        AppConfigSvc["AppConfigurationService"]
    end

    LoginPage --> LoginVM
    RegisterPage --> RegisterVM
    LobbyPage --> LobbyVM
    GameBoardPage --> GameBoardVM

    LobbyVM --> Messenger
    GameBoardVM --> Messenger

    GameStateSvc --> Messenger
    GameStateSvc --> SignalRSvc
    GameStateSvc --> LocalStorageSvc
    LoginVM --> ApiSvc
    LoginVM --> AuthSessionSvc
    LoginVM --> SignalRSvc

Layered architecture diagram of the .NET MAUI client showing Views, ViewModels, WeakReferenceMessenger, and Service implementations.

Dependency Injection & Service Registration (MauiProgram.cs)

  • Singletons: IAppConfigurationService (AppConfigurationService), ILocalStorageService (LocalStorageService), IAuthSessionService (AuthSessionService), IApiService (ApiService), ISignalRService (SignalRService), IGameStateService (GameStateService), LobbyViewModel, GameBoardViewModel.
  • Transients: LoginViewModel, RegisterViewModel.
  • Service Locator Helper: ServiceHelper.Services captures app.Services at startup to allow static access where constructor DI is unavailable. Provides GetRequiredService<T>() for use in contexts like WeakReferenceMessenger handlers.

Decoupled Messaging Infrastructure (WeakReferenceMessenger)

To avoid memory leaks caused by long-lived event subscriptions in ViewModels, GameStateService translates raw ISignalRService hub events into strongly-typed messages dispatched via WeakReferenceMessenger.Default:

  • LobbyCreatedMessage: Dispatched when a new game lobby code is issued by the server.
  • GameStateChangedMessage: Dispatched when public game state or player list updates.
  • GameStartedMessage: Dispatched when the game transitions to InProgress.
  • PlayerHandChangedMessage: Dispatched when a player receives or updates private phrase cards.
  • ChallengeReceivedMessage: Dispatched when a player is directly targeted by a slip challenge.
  • ErrorOccurredMessage: Dispatched when a hub or network error occurs.
  • ConnectionStateChangedMessage: Dispatched when SignalR connection drops or reconnects.
  • SyncStateChangedMessage: Dispatched when the sync status changes (syncing, idle, queue count). Carries a SyncStateInfo record with IsSyncing, StatusText, and PendingActions fields.

ViewModels implement IRecipient<TMessage> and register via WeakReferenceMessenger.Default.RegisterAll(this) to receive automated UI updates.

Real-Time Connection Resilience & Auto-Reconnect

SignalRService configures exponential backoff for WebSocket reconnection:

_hubConnection = new HubConnectionBuilder()
    .WithUrl(_configuration.HubUrl, options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(_authSession.AccessToken);
    })
    .WithAutomaticReconnect(new[] {TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)})
    .Build();

When connection drops, SignalRService emits ConnectionStateChanged with false. Upon reconnection, it emits ConnectionStateChanged with true, and GameStateService automatically triggers ResyncAsync() to fetch current state via RequestGameStateAsync() and flush locally queued offline actions.

The AccessTokenProvider reads _authSession.AccessToken at invocation time (not cached at connection build time), ensuring the latest token is used on reconnects. The SemaphoreSlim _connectLock prevents concurrent connect/disconnect operations.

On the server side, GameHub.OnConnectedAsync automatically detects if the reconnecting user was already in a game (Lobby or InProgress), stores the new ConnectionId, and re-joins the SignalR lobby group. OnDisconnectedAsync clears the ConnectionId for all affected players to prevent messages from being sent to dead connections.

Offline Action Queue with Retry

When the SignalR connection is unavailable, GameStateService provides EnqueueActionAsync(method, arguments) to persist actions locally. Each action stores the hub method name and serialized arguments as JSON. On reconnect, FlushQueuedActionsCoreAsync replays actions sequentially through SignalRService.SendQueuedActionAsync. Failed actions retry up to MaxQueueRetries (3) times; actions exceeding the retry limit are dropped and an error message is dispatched. The queue is persisted to LocalStorageService after each modification.

Dual-Layer Storage Security Model (LocalStorageService)

To satisfy security and privacy constraints:

  1. SecureStorage (Platform Encrypted Storage): Used strictly for AuthTokenKey (auth_token). Prevents plaintext access to active JWT credentials.
  2. Preferences (Application Key-Value Settings): Used for non-sensitive cached data (auth_user, game_state, player_hand, and queued_actions).

5. Concurrency Safety: IDbContextFactory

SignalR hubs process concurrent requests from multiple clients over persistent connections. Using a standard Scoped DbContext in SignalR leads to thread conflict exceptions (InvalidOperationException: A second operation was started on this context instance before a previous operation completed).

Resolution via IDbContextFactory

SlipItIn solves this by registering EF Core with AddDbContextFactory<SlipItInDbContext>:

  • Every method call inside GameHub.cs, AuthController.cs, and GameService.cs creates a short-lived, isolated DbContext session using using var context = _contextFactory.CreateDbContext().
  • OnTokenValidated in Program.cs uses CreateDbContextAsync to verify user existence during JWT validation.
  • Benefit: Thread-safe database operations across simultaneous challenges, card transfers, and lobby updates without race conditions.

6. Data Privacy Isolation Model

To prevent players from inspecting opponent cards via network trace analysis, SlipItIn strictly separates public and private data DTOs:

  • GameStateDto (Public): Broadcast to all players in a lobby. Contains public game information (GameId, LobbyCode, Status, CurrentRound, RoundTimeRemaining) and player summaries (PlayerId, Username, Score, IsReady, CardCount). Crucially, no card text is included. The CardCount field represents only active (unused) cards.
  • PlayerHandDto (Private): Direct unicast message sent only to the specific player's SignalR ConnectionId via Clients.Client(connectionId).SendAsync("PlayerHandUpdated", hand). Contains card text (CardId, PhraseId, Text, IsUsed). Only cards from the active round are included.

This architecture guarantees that players cannot see opponent phrase cards, even if they inspect raw WebSocket packets.


7. Guidance for Future Agents & Developers

When extending or modifying the system architecture:

  • When to Consult: Consult this document before adding new real-time SignalR methods, introducing client ViewModels, modifying storage mechanisms, or altering server authentication logic.
  • Runtime Invariants:
    • GameHub methods must validate caller claims via GetAuthenticatedUserId() and the appropriate Validate*Async method before every state mutation.
    • Client ViewModels must consume WeakReferenceMessenger messages rather than subscribing directly to ISignalRService C# events.
    • JWT tokens must never be persisted in Preferences; always use SecureStorage.
    • GameService must use IDbContextFactory and create short-lived contexts — never inject a shared DbContext.
    • New GameHub methods must handle UnauthorizedAccessException, InvalidOperationException, and general Exception with appropriate error messages sent to the caller.
  • Extension Points:
    • New real-time server messages: Add method to GameHub, add event to ISignalRService, add message class in SlipItIn/Messages/, update GameStateService listener.
    • New ViewModels: Register as Transient (dialogs/auth) or Singleton (main navigation screens) in MauiProgram.cs.
    • New queued offline actions: Use GameStateService.EnqueueActionAsync with the hub method name and arguments.
  • Primary Source Files:
    • SlipItIn/MauiProgram.cs
    • SlipItIn/Services/SignalRService.cs
    • SlipItIn/Services/GameStateService.cs
    • SlipItIn/Services/LocalStorageService.cs
    • SlipItIn.Server/Program.cs
    • SlipItIn.Server/Hubs/GameHub.cs
    • SlipItIn.Server/Services/GameService.cs
    • SlipItIn.Server/Controllers/AuthController.cs
  • Focused Checks: Build and verify project references across client, server, and shared libraries.
  • Minimal Validation Command:
    dotnet build SlipItIn.slnx