13 KiB
type, title, description, tags, openwiki
| type | title | description | tags | openwiki | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 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, and dual-layer local storage. |
|
|
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.Serverwith a direct reference to the PostgreSQL resource. - Registers the client project
SlipItInwith service discovery references toSlipItIn.Server.
- Provisions a PostgreSQL database container (
- Service Defaults (
SlipItIn.ServiceDefaults/Extensions.cs):- Configures OpenTelemetry logging, metrics (AspNetCore, HttpClient, Runtime), and tracing.
- Exposes standardized health check endpoints (
/healthand/alive). - 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
Program.cs:- Registers
AddServiceDefaults(),AddNpgsqlDataSource("postgresdb"), andAddDbContextFactory<SlipItInDbContext>(). - Configures JWT Bearer authentication with custom
OnMessageReceivedtoken resolution for SignalR. - Automatically executes database migrations (
db.Database.Migrate()) at startup.
- Registers
AuthController.cs:- Manages user registration (
/api/auth/register), authentication (/api/auth/login), and profile retrieval (/api/auth/me). - Uses
BCrypt.Netfor secure password hashing. - Issues JWT tokens signed with
Jwt:Key, containingClaimTypes.NameIdentifier,ClaimTypes.Name, andClaimTypes.Email.
- Manages user registration (
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.
- Real-time SignalR hub mapped to
GameService.cs:- Implements core business logic: game creation, player joins, card dealing, slip submission, challenge creation, and resolution.
- Adds
hostPlayerdirectly togame.Playersbeforecontext.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;
}
};
Claims Validation & Access Control in GameHub.cs
Inside GameHub.cs, claims are extracted directly from the authenticated SignalR context:
GetAuthenticatedUserId(): ExtractsContext.User?.FindFirst(ClaimTypes.NameIdentifier). ThrowsUnauthorizedAccessExceptionif missing or invalid.ValidatePlayerAccessAsync(playerId, authUserId): Queries the database to verify thatPlayer.UserIdmatches the authenticated user ID.ValidateHostAccessAsync(gameId, authUserId): Verifies thatGame.HostIdmatches the authenticated user ID before allowing lobby configuration or game start.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 & Verify BCrypt Hash
Auth-->>Client: 200 OK (JWT Access Token)
Client->>Hub: Connect WebSocket /hubs/game?access_token=JWT
Hub->>Hub: Validate JWT Signature & Claims
Client->>Hub: JoinLobby(lobbyCode)
Hub->>Hub: GetAuthenticatedUserId()
Hub->>Service: JoinGameAsync(lobbyCode, userId, connectionId)
Hub-->>Client: Broadcast "PlayerJoined" (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 (GameStateChanged, PlayerHandChanged, ChallengeReceived)"]
end
subgraph Service_Layer["Service Layer"]
GameStateSvc["GameStateService"]
SignalRSvc["SignalRService"]
AuthSessionSvc["AuthSessionService"]
ApiSvc["ApiService"]
LocalStorageSvc["LocalStorageService"]
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.Servicescapturesapp.Servicesat startup to allow static access where constructor DI is unavailable.
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 toInProgress.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.
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<string?>(token);
})
.WithAutomaticReconnect([TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)])
.Build();
When connection drops, SignalRService emits ConnectionStateChanged. Upon reconnection, GameStateService automatically triggers ResyncAsync() to fetch current state via RequestGameStateAsync() and flush locally queued offline actions.
Dual-Layer Storage Security Model (LocalStorageService)
To satisfy security and privacy constraints:
SecureStorage(Platform Encrypted Storage): Used strictly forAuthTokenKey(auth_token). Prevents plaintext access to active JWT credentials.Preferences(Application Key-Value Settings): Used for non-sensitive cached data (auth_user,game_state,player_hand, andqueued_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, andGameService.cscreates a short-lived, isolatedDbContextsession usingusing var context = _contextFactory.CreateDbContext(). - 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.PlayerHandDto(Private): Direct unicast message sent only to the specific player's SignalRConnectionId. Contains card text (CardId,PhraseId,Text,IsUsed).
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:
GameHubmethods must validate caller claims viaGetAuthenticatedUserId()andValidatePlayerAccessAsync().- Client ViewModels must consume
WeakReferenceMessengermessages rather than subscribing directly toISignalRServiceC# events. - JWT tokens must never be persisted in
Preferences; always useSecureStorage.
- Extension Points:
- New real-time server messages: Add method to
GameHub, add event toISignalRService, add message class inSlipItIn/Messages/, updateGameStateServicelistener. - New ViewModels: Register as Transient (dialogs/auth) or Singleton (main navigation screens) in
MauiProgram.cs.
- New real-time server messages: Add method to
- Primary Source Files:
SlipItIn/MauiProgram.csSlipItIn/Services/SignalRService.csSlipItIn/Services/GameStateService.csSlipItIn/Services/LocalStorageService.csSlipItIn.Server/Hubs/GameHub.csSlipItIn.Server/Services/GameService.cs
- Focused Checks: Build and verify project references across client, server, and shared libraries.
- Minimal Validation Command:
dotnet build SlipItIn.slnx