Berichten, systeemberichten, notificaties en privéthreads
13.1 Doel en scope
Dit hoofdstuk beschrijft de technische inrichting van communicatie binnen OefenHub. Het hoofdstuk werkt uit hoe mailboxitems, systeemberichten, privéberichtthreads, thread-events, systeemnotificaties, badges, integratie met e-mailafhandeling en berichtgerelateerde realtime updates technisch worden gerealiseerd binnen de gekozen middel-zware modulaire monoliet.
De communicatiefunctionaliteit wordt technisch gescheiden van de domeinen die communicatie veroorzaken. Relatiebeheer, meldingen, gedeelde oefeningen, live meekijken en beheerflows mogen communicatie laten aanmaken, maar blijven eigenaar van hun eigen bronmutaties en domeinregels. De communicatiemodule is eigenaar van mailboxcommunicatie, privéthreads, systeemberichten, leesstatussen, participantzichtbaarheid en berichtbadges. Applicatiegestuurde externe e-mail wordt door OefenHub.Mail afgehandeld en alleen via publieke contracts aangeroepen.
Belangrijke inputbronnen zijn:
- Technisch Ontwerp: applicatielagen, projectstructuur en dependency-richting
- Technisch Ontwerp: oefenruns, voortgang, resultaten en PDF-brondata
- Technisch Ontwerp: realtime live meekijken met SignalR
- Technisch Ontwerp: readmodels, tellers, badges, caching en materialisatie
- Technisch Ontwerp: background jobs, TickerQ en periodieke verwerking
- Technisch Ontwerp: logging, audit, securitylogging en technische foutafhandeling
- Technisch Ontwerp: security, infrastructuur, secrets en omgevingen
- Technisch Ontwerp: frontend Blazor, routing, state en componentopbouw
13.2 Afbakening
Dit hoofdstuk beschrijft technische realisatie en ontwerpkeuzes. Nieuwe functionele eisen ontstaan niet in dit hoofdstuk. Wanneer implementatie aantoont dat functioneel gedrag ontbreekt of wijzigt, wordt dit teruggelegd in Functioneel Ontwerp, Software Requirements Specification, schermdocumentatie, usecases of database-informatie voordat het Technisch Ontwerp als implementatiebron wordt aangepast.
| Onderwerp | In dit hoofdstuk | Buiten dit hoofdstuk |
|---|---|---|
| Mailboxweergave | Technische bron, query-services en readmodels | Schermlabels en exacte UI-copy |
| Systeemberichten | Opslag, verwijzingen, routinginput en leesstatus | Functionele tekstinhoud per template |
| Privéberichten | Threadmodel, participants, rich text sanitizing en retentie | Vrije chatfunctionaliteit buiten bestaande scope |
| Systeemnotificaties | Technische scheiding van mailbox en popups | Beheer-UI voor inhoudelijke notificatieconfiguratie |
| Badges | Afleiding, realtime update en onderdrukking tijdens oefenrun | Visueel ontwerp van badge-iconen |
| SignalR | Communicatie-updates als transport | Live-meekijktransport, dat in hoofdstuk 15 staat |
| Retentie | Technische verwerking en cleanup | Juridisch bewaarbeleid buiten de vastgelegde scope |
| Cross-module workflows | Communicatie-ingangen en transaction boundary | Domeinregels van veroorzakende modules |
| E-mailafhandeling | Integratiepunt richting OefenHub.Mail wanneer interne communicatie een externe mail moet veroorzaken | Identity-providerinterne mailflows zoals wachtwoordreset of provider-e-mailverificatie |
13.3 Module-eigenaarschap
De communicatiefuncties worden ondergebracht in het moduleproject OefenHub.Communication.
| Onderdeel | Keuze |
|---|---|
| Project | OefenHub.Communication |
| DbContext | CommunicationDbContext |
| Database schema | communication |
| Tabellen | PascalCase binnen schema communication |
| Publieke toegang | via Contracts |
| Interne implementatie | internal |
| Readmodels | Models/ReadModels |
| Scheduling-integratie | via publieke schedulingcontracten |
| E-mailtransport | via OefenHub.Mail; Communication schrijft niet direct naar SMTP of mailtabellen |
| Realtime transport | via Web/SignalR-integratie, niet als bron van waarheid |
De communicatiemodule wordt geen generieke workflowmodule. Zij is alleen eigenaar van communicatieobjecten en communicatieverwerking. Een ticket blijft eigendom van OefenHub.Support, een relatie-uitnodiging blijft eigendom van OefenHub.Relationships en een gedeelde oefening blijft eigendom van OefenHub.Practice.
13.4 Projectstructuur
De module volgt de standaard moduleprojectstructuur uit hoofdstuk 03.
OefenHub.Communication/
Contracts/
ISystemMessageService.cs
IPrivateMessageService.cs
IMailboxReader.cs
IMessageBadgeReader.cs
ISystemNotificationReader.cs
Models/
Enums/
Data/
CommunicationDbContext.cs
Entities/
Configurations/
Migrations/
Models/
Commands/
Queries/
ReadModels/
Enums/
Services/
Interfaces/
Events/
Helpers/
Extensions/
De map Contracts bevat alleen publieke ingangen die andere modules of OefenHub.Web mogen gebruiken. Entities, EF-configuraties, repositories en interne services blijven module-intern.
13.5 Communicatiedomeinen
Binnen OefenHub.Communication worden vier technische communicatiedomeinen onderscheiden.
| Domein | Doel | Bron van waarheid |
|---|---|---|
| Systeemberichten | Mailboxitems die door OefenHub of een module worden aangemaakt | communication.SystemMessages |
| Privéthreads | Gebruikerscommunicatie met participants en berichten | thread-, participant- en message-tabellen |
| Thread-events | Systeemachtige gebeurtenissen binnen een privéthread | thread-eventtabel binnen communication |
| Systeemnotificaties | Sitebrede overlay/notificatie na frontpageload | notificatieconfiguratie, niet mailbox |
Systeemberichten, privéthreads en systeemnotificaties blijven technisch en functioneel gescheiden. Zij mogen in de UI dicht bij elkaar zichtbaar zijn, maar worden niet in één generieke communicatietabel samengevoegd.
13.5.1 Canonieke routes en thread-first detailmodel
De Web-laag gebruikt de volgende canonieke routes voor de eerste mailboxbaseline:
| Route | Doel | Autorisatiebron |
|---|---|---|
/messages | mailboxoverzicht | actuele gebruiker + mailboxquery |
/messages/system/{messageId} | systeemberichtdetail | SystemMessages.RecipientUserId |
/messages/thread/{threadId} | privéthreaddetail | PrivateMessageThreadParticipants.UserId + zichtbaarheid |
/messages/system/{messageId}/open | readstate eerst bijwerken en daarna naar systeemberichtdetail redirecten | SystemMessages.RecipientUserId |
/messages/thread/{threadId}/open | participant-readstate eerst bijwerken en daarna naar threaddetail redirecten | PrivateMessageThreadParticipants.UserId + zichtbaarheid |
POST /messages/thread/{threadId}/timeline/older | oudere timeline-items zonder volledige page refresh laden | PrivateMessageThreadParticipants.UserId + zichtbaarheid + protected cursor |
De /open-route voor privéthreads bepaalt vóór het muteren van de readstate of de detailpagina naar #new of #latest moet springen. Hash-ankers verwijzen naar echte timelineposities en zijn toegestaan. Technische status-querystrings voor mutatiefeedback blijven ongewenst; tijdelijke feedback gebruikt het centrale flash-/notificatiepatroon.
Een privébericht krijgt geen primaire detailroute op individueel messageId. Individuele berichten worden alleen binnen hun threadcontext geladen. Dit voorkomt dat latere reply/delete/group-threadfunctionaliteit op een één-op-één- of los-message-detailmodel wordt gebouwd.
De /open-routes zijn geen aparte detailbron. Zij bestaan alleen om vanuit het overzicht eerst de eigen readstate te muteren en daarna de echte detailroute te renderen, zodat headerbadge, shell en detailweergave dezelfde actuele server-side teller kunnen gebruiken.
13.6 Systeemberichten
Systeemberichten zijn mailboxitems die door OefenHub of door domeinmodules worden aangemaakt om een gebruiker te informeren of een vervolgactie aan te bieden.
13.6.1 Eigenschappen
Een systeembericht bevat technisch minimaal:
| Veldtype | Betekenis |
|---|---|
| Ontvanger | interne gebruiker voor wie het bericht zichtbaar is |
| Template of berichttype | verwijzing naar de functionele berichtsoort |
| Onderwerp/tekst | gerenderde of opgeslagen berichtinhoud |
| EntityType | type domeinobject waarnaar het bericht verwijst |
| EntityId | identifier van het concrete domeinobject |
| ReadAtUtc | leesstatus voor de ontvanger |
| CreatedAtUtc | aanmaakmoment |
| CorrelationId | technische herleidbaarheid door workflow heen |
De exacte kolommen blijven vastgelegd in database-informatie. Het Technisch Ontwerp legt de technische eigenaarschap- en verwerkingsregels vast.
Bij aanmaak bevat Subject/Body de gerenderde tekst voor dat specifieke runtimebericht. Er wordt geen verplichte FK naar SystemMessageTemplates of een templateversie toegevoegd. Functionele herleidbaarheid loopt via scenario, EntityType/EntityId, events en logs; de templatehistory is bedoeld om templatewijzigingen op tijdlijnniveau te reconstrueren, niet om elk verzonden bericht hard te koppelen.
13.6.2 EntityType en EntityId
Systeemberichten gebruiken geen losse database-URL als bron van navigatie. De combinatie EntityType + EntityId vormt een functionele verwijzing naar een domeinobject. De frontend bepaalt op basis daarvan welke route, detailpagina, modal of veilige niet-beschikbaarafhandeling van toepassing is.
Toegestane verwijstypen worden expliciet als gesloten set beheerd. Voor de eerste technische baseline gelden onder meer:
| EntityType | Eigenaarmodule | Voorbeeldgebruik |
|---|---|---|
RelationshipInvitation | OefenHub.Relationships | uitnodiging accepteren of afwijzen |
Ticket | OefenHub.Support | meldingdetail openen |
PrivateMessageThread | OefenHub.Communication | privéthread openen |
SharedExercise | OefenHub.Practice | ontvangen gedeelde oefening openen |
Een systeembericht mag alleen naar een bestaand of functioneel herstelbaar domeinobject verwijzen. Wanneer het doelobject niet meer toegankelijk, verlopen of afgehandeld is, blijft het systeembericht als historisch mailboxitem bestaan, maar wordt de vervolgactie veilig geblokkeerd.
13.6.3 Kritieke systeemberichten
Een systeembericht kan functioneel kritiek zijn. De communicatie mag daarom niet generiek als niet-kritieke naverwerking worden behandeld.
| Situatie | Kritiek? | Verwerking |
|---|---|---|
| Relatie-uitnodiging waarbij systeembericht de primaire ingang is | Ja | atomair met uitnodiging |
| Ticketupdate waarbij gebruiker de melding ook via Mijn meldingen kan openen | Afhankelijk van flow | per workflow bepalen |
| Gedeelde oefening met aparte ontvangen-overzichtspagina | Mogelijk niet | retrybaar indien overzicht voldoende is |
| Badge-update | Nee | afgeleid/retrybaar/herbouwbaar |
| Readmodel-update | Nee | afgeleid/retrybaar/herbouwbaar |
Per workflow wordt vastgesteld of het systeembericht onderdeel is van de functionele transactie. Als het bericht de enige of primaire ingang is voor een noodzakelijke vervolgactie, wordt het als kritiek behandeld.
13.6.4 Voorbeeld: relatie-uitnodiging
Relationships-module ontvangt command: nodig ouder/voogd uit
1. valideer actor, rolcontext en e-mailadres
2. controleer conflicterende uitnodigingen en relaties
3. start transaction boundary voor kritieke stappen
4. maak RelationshipInvitation aan
5. maak SystemMessage aan via Communication-contract
6. commit
7. publiceer eventuele niet-kritieke badge/update-events
Wanneer stap 5 faalt en het systeembericht de primaire ingang voor acceptatie is, wordt de uitnodiging niet als succesvol aangemaakt beschouwd.
13.7 Privéberichtthreads
Privéberichten worden technisch als threads/conversaties gemodelleerd. Een thread bestaat uit participants, berichten en thread-events. Zichtbaarheid, gelezenstatus en verwijderen worden per participant vastgelegd.
13.7.1 Lagen
| Laag | Verantwoordelijkheid |
|---|---|
| Thread | conversatie-identiteit, onderwerp en lifecycle |
| Participant | gebruiker, deelname, zichtbaarheid en readstate |
| Message | inhoudelijk privébericht binnen thread |
| ThreadEvent | systeemachtige gebeurtenis binnen thread |
Deze scheiding voorkomt dat het verwijderen of lezen door één deelnemer effect heeft op de mailbox van een andere deelnemer.
13.7.2 Participantgebonden zichtbaarheid
Het verwijderen van een privébericht of thread door een gebruiker is participantgebonden. De thread en berichten worden niet hard verwijderd zolang andere deelnemers de thread nog functioneel zichtbaar hebben of zolang retentie/auditregels behoud vereisen.
| Actie | Technisch effect |
|---|---|
| Thread verwijderen door participant | eigen mailboxzichtbaarheid wordt uitgeschakeld |
| Andere participant opent thread | blijft zichtbaar voor die participant |
| Nieuwe reply nadat een participant heeft verlaten | reply wordt geblokkeerd wanneer geen andere actieve deelnemer beschikbaar is; verwijderde participant wordt niet automatisch opnieuw zichtbaar |
| Retentie verlopen | cleanup verwerkt privéberichtdata volgens retentiebeleid |
13.7.3 Gelezen en ongelezen
Leesstatus wordt per participant bijgehouden. Een thread kan voor de ene deelnemer gelezen zijn en voor een andere deelnemer ongelezen.
Bij voorkeur wordt niet per bericht een losse readregel voor iedere participant opgeslagen als dat niet nodig is. Een participant kan bijvoorbeeld een LastReadMessageId, LastReadEventId of LastReadAtUtc hebben, afhankelijk van de gekozen databaseuitwerking. De exacte vorm volgt uit database-informatie; het Technisch Ontwerp-principe is dat readstate participantgebonden is.
Thread-events na de laatst gelezen positie kunnen een thread opnieuw ongelezen maken.
13.7.4 Eén timeline en windowed laden
De privéthreaddetailpagina gebruikt één timeline-readmodel. Het readmodel sorteert zichtbare timeline-items chronologisch van oud naar nieuw binnen het geladen venster. Het venster bevat bij openen minimaal de relevante positie voor de gebruiker:
- de eerste nieuwe activiteit wanneer de thread ongelezen is;
- de laatste activiteit wanneer de thread gelezen is.
De UI plaatst vervolgens een marker Nieuwe berichten of een anker Laatste bericht. Het markeren als gelezen gebeurt nadat de oude readstate is gebruikt om die positie te bepalen. Eigen berichten en eigen thread-events worden voor de actor niet als nieuwe activiteit beschouwd.
Lange threads worden niet onbeperkt geladen. De implementatie gebruikt configureerbare limieten via options/appsettings:
OefenHub:Messages:ThreadTimeline:InitialItemCount = 25
OefenHub:Messages:ThreadTimeline:OlderBatchSize = 20
OefenHub:Messages:ThreadTimeline:MaxBatchSize = 100
InitialItemCount is het standaard aantal timeline-items bij het openen van een gesprek. OlderBatchSize is het aantal extra oudere items per klik op Eerdere berichten laden. MaxBatchSize is een server-side veiligheidslimiet voor één ophaalactie; het is geen maximum op het totaal aantal items dat uiteindelijk zichtbaar mag zijn. De waarden zijn performance-/UX-configuratie en horen initieel in appsettings/options. System settings in de database zijn pas nodig wanneer beheer of operations deze waarden runtime moeten kunnen aanpassen.
13.7.5 Oudere timeline-items laden
Oudere berichten laden gebeurt zonder user-aanpasbare querystring. De aanbevolen route is een beveiligde POST, bijvoorbeeld:
POST /messages/thread/{threadId}/timeline/older
De route gebruikt antiforgery, current-user uit de authenticatiecontext, server-side participantautorisatie en een opaque cursor in de request body. De cursor is geen autorisatiebewijs en geen door de gebruiker te interpreteren paginanummer. De cursor is een met ASP.NET Data Protection beschermde payload. Conceptueel bevat die payload minimaal:
ThreadId;UserId;- het oudste geladen
OccurredAtUtc; - het oudste geladen timeline-item-id;
- een purpose/versie voor deze timelinecursor.
De weblaag serialiseert deze payload, beschermt hem met Data Protection en geeft de output als opaque string terug als nextCursor. Bij een volgend verzoek wordt de cursor eerst ge-unprotect. Daarna controleert de server opnieuw dat de cursor bij de route-thread en de ingelogde gebruiker hoort en dat de gebruiker nog actieve participanttoegang heeft. Pas daarna worden oudere items opgehaald met een server-side begrensde batchgrootte. Een gewijzigde, verlopen of contextvreemde cursor faalt veilig met gebruikersgerichte foutfeedback en zonder threadinhoud te lekken.
Bij het toevoegen van oudere items bovenaan de timeline bewaart de client de visuele scrollpositie. Na het invoegen wordt de scrollpositie gecorrigeerd met het hoogteverschil, zodat de gebruiker op dezelfde plek in het gesprek blijft. De server blijft de bron van waarheid voor welke items de gebruiker mag ontvangen.
De JSON-response bevat alleen renderbare oudere timelinefragmenten, een nieuwe nextCursor voor de volgende batch en een hasMore-indicatie. Querystringcursoring wordt bewust vermeden.
13.7.6 Thread- en participantpresentatie
PrivateMessageThreads krijgt een stabiele visuele presentatie voor overzicht en detail:
- een threadkleur, bijvoorbeeld
DisplayColorHex; - een icon key uit een kleine allowlist, bijvoorbeeld
IconKey.
Deze waarden worden bij threadaanmaak gegenereerd en zijn voor alle participants gelijk. De icon key is geen HTML of vrije CSS, maar een sleutel naar een beheerde iconenset.
PrivateMessageThreadParticipants krijgt daarnaast een threadspecifieke participantkleur, bijvoorbeeld DisplayColorHex of ThreadAccentColorHex. Die kleur wordt gebruikt om berichtballonnen, deelnemerlabels of avatars binnen de thread herkenbaar te maken. De kleur is contextspecifiek en wijzigt niet de globale profiel- of avatarweergave van de gebruiker.
13.8 Thread-events
Thread-events zijn systeemachtige gebeurtenissen binnen een privéthread. Zij zijn geen mailbox-systeemberichten.
Voorbeelden:
| Eventtype | Voorbeeldweergave |
|---|---|
SubjectChanged | gebruiker wijzigde onderwerp van A naar B |
ParticipantLeft | gebruiker heeft het gesprek verlaten; actor wordt in de eventtekst opgenomen |
ParticipantAdded | gebruiker is toegevoegd aan de thread |
Thread-events maken deel uit van de threadtijdlijn en kunnen meetellen voor de ongelezenstatus. Zij worden niet als afzonderlijke SystemMessages opgeslagen.
13.9 Privéberichtinhoud en rich text
Privéberichten ondersteunen alleen een beperkte en veilige subset van opmaak. Vrije HTML, JavaScript, externe scripts, inline eventhandlers en actieve content zijn niet toegestaan.
| Onderdeel | Beleid |
|---|---|
| Vet/cursief/onderstreept | toegestaan indien veilig gesanitized |
| Tekstgroottes | beperkt tot ondersteunde waarden |
| Opsommingen/genummerde lijsten | toegestaan indien veilig gesanitized |
| Links | alleen indien expliciet toegestaan en veilig gevalideerd |
| Afbeeldingen/bijlagen | buiten de eerste technische baseline, tenzij via een expliciet technisch ontwerpbesluit apart gespecificeerd |
| HTML/JavaScript | niet toegestaan |
Sanitizing gebeurt server-side vóór opslag of uiterlijk vóór rendering. De clienteditor is gebruiksvriendelijkheid, geen beveiligingsgrens.
Voor privéberichten gelden de volgende V1.0-grenzen:
| Onderdeel | Technische grens |
|---|---|
| Onderwerp | maximaal 120 tekens na trimming en normalisatie. |
| Zichtbare berichttekst | maximaal 4.000 tekens na sanitizing. |
| Opgeslagen rich-text body | maximaal 12 KiB genormaliseerde veilige HTML. |
| Bijlagen | niet toegestaan in de eerste baseline. |
De server valideert deze grenzen bij nieuw bericht en antwoord. Bij overschrijding wordt geen bericht of thread-event opgeslagen en krijgt de gebruiker veilige validatiefeedback zonder technische details.
13.10 Systeemnotificaties
Systeemnotificaties zijn sitebrede of doelgroepgerichte overlays/notificaties die na frontpageload kunnen verschijnen. Zij zijn geen mailbox-systeemberichten en geen popupregister-popups.
| Kenmerk | Systeemnotificatie | Systeembericht | Popupregister-popup |
|---|---|---|---|
| Plaats | boven frontpage na load | mailbox/berichtenoverzicht | contextuele UI-feedback |
| Persistente mailboxstatus | nee | ja | nee |
| ReadAtUtc | nee | ja | nee |
| DisplayRule | ja | nee | popupafhankelijk |
| Beheerbare inhoud | ja | via templates | ja |
| Doel | tijdelijke sitecommunicatie | persoonlijke of objectgerichte communicatie | actiefeedback/bevestiging |
DisplayRule = Always wordt niet permanent als gezien geregistreerd. Browsergebonden onderdrukking zoals OncePerBrowser mag client-side worden vastgelegd zolang dit geen persoonsgegevens, autorisatiedata of rolcontext bevat.
Tijdens een actieve leerling-oefenrun worden systeemnotificatie-overlays niet getoond. Na verlaten, onderbreken of afronden van de oefencontext wordt opnieuw beoordeeld of notificaties zichtbaar moeten worden.
13.11 Badges en tellerupdates
Berichtenbadges worden afgeleid uit systeemberichten, privéthreadparticipants, readstates en thread-events. De badge is een readmodel of queryresultaat, geen zelfstandig bronrecord.
| Badgebron | Afleiding |
|---|---|
| Ongelezen systeemberichten | ReadAtUtc ontbreekt voor ontvanger |
| Ongelezen privéthreads | participant-readstate loopt achter op laatste bericht/event |
| Verwijderde privéthreads | tellen niet mee voor betreffende participant |
| Verlopen/retentie verwijderde privédata | telt niet mee nadat zichtbaarheids- of retentieregels dit uitsluiten |
13.11.1 Afleidingsvrije oefencontext
Tijdens een actieve leerling-oefenrun worden zichtbare badges, meldingsindicaties en notificatie-overlays in de UI verborgen of uitgesteld. De onderliggende server-side ongelezenstatus wordt wel bijgewerkt.
De communicatiemodule blijft dus correcte readstates en badgequery’s leveren. OefenHub.Web bepaalt op basis van de actuele oefencontext of de badge op dat moment zichtbaar wordt gemaakt.
Nieuw bericht komt binnen tijdens oefenrun
- Communication slaat bericht/readstate correct op
- Badgequery zou ongelezen aantal verhogen
- Web toont badge niet tijdens actieve oefencontext
- Na verlaten/afronden oefening vraagt Web badge opnieuw op
13.12 Readmodels en query-services
Mailbox-, badge- en notificatieweergaven worden via publieke query-services ontsloten. Andere modules en OefenHub.Web mogen niet rechtstreeks op CommunicationDbContext of communication-entities lezen.
Voorbeelden van publieke query-services:
public interface IMailboxReader
{
Task<MailboxOverviewReadModel> GetMailboxAsync(MailboxQuery query, CancellationToken cancellationToken);
Task<MessageDetailReadModel> GetMessageDetailAsync(MessageDetailQuery query, CancellationToken cancellationToken);
}
public interface IMessageBadgeReader
{
Task<MessageBadgeReadModel> GetBadgeAsync(MessageBadgeQuery query, CancellationToken cancellationToken);
}
Module-eigen readmodels staan onder:
OefenHub.Communication/
Models/
ReadModels/
MailboxOverviewReadModel.cs
MessageDetailReadModel.cs
MessageBadgeReadModel.cs
PrivateThreadReadModel.cs
Samengestelde UI-viewmodels horen in OefenHub.Web/ViewModels of OefenHub.Web/PageComposition en worden opgebouwd via deze publieke query-services.
13.13 Publieke contracts
De communicatiemodule publiceert alleen contracten die nodig zijn voor andere modules of OefenHub.Web.
| Contract | Doel |
|---|---|
ISystemMessageService | systeemberichten aanmaken of markeren binnen toegestane flows |
IPrivateMessageService | privéberichtthreads starten, beantwoorden, verwijderen en leesstatus verwerken |
IMailboxReader | mailboxoverzicht en berichtdetails lezen |
IMessageBadgeReader | ongelezen badges afleiden |
ISystemNotificationReader | actieve systeemnotificaties voor context bepalen |
ICommunicationTemplateRenderer | templates veilig renderen indien technisch nodig |
Interne services zoals sanitizers, repositories, EF-querybuilders en dispatchers blijven internal.
13.14 Cross-module integratie
13.14.1 Verantwoordelijkheden
| Veroorzakende module | Communicatiegebruik | Eigenaarschap bronactie |
|---|---|---|
Relationships | uitnodigingsbericht en statusupdates | Relationships |
Practice | melding gedeelde oefening | Practice |
Support | melding aangemaakt, info gevraagd, oplossing geplaatst | Support |
Catalog | informatie over nieuwe categorie/oefening indien van toepassing | Catalog |
Admin | beheer van templates, notificaties en content | Admin of configuratie-eigenaar |
LiveMonitoring | systeembericht over meekijken indien functioneel vereist | LiveMonitoring |
Een domeinmodule mag communicatie niet aanmaken door direct in communication.* tabellen te schrijven. Zij gebruikt uitsluitend het publieke communicatiecontract.
13.14.2 Transaction boundary
Per workflow wordt expliciet vastgesteld of communicatie onderdeel is van de functionele transactie.
| Communicatiestap | Transactiebeleid |
|---|---|
| Primaire ingang voor vervolgactie | atomair met bronactie |
| Juridisch/functioneel noodzakelijke bevestiging | atomair of expliciet beheerbaar falen |
| Informatieve melding met alternatieve ingang | retrybaar toegestaan |
| Badge-update | afgeleid/retrybaar/herbouwbaar |
| Realtime update | na commit, transport mag falen zonder bronrollback |
Bij twijfel wordt een communicatiestap als kritiek behandeld totdat de workflow expliciet onderbouwt dat retrybaar falen veilig is.
13.15 Realtime berichtupdates
Realtime updates voor badges of mailboxwijzigingen mogen via SignalR worden doorgegeven, maar SignalR is transport en geen bron van waarheid.
De bron blijft:
| Informatie | Bron |
|---|---|
| Ongelezen systeemberichten | communication.SystemMessages |
| Ongelezen privéthreads | participant-readstate en threadevents |
| Mailboxlijst | mailboxquery/readmodel |
| Systeemnotificatiezichtbaarheid | notificatieconfiguratie + contextcontrole |
Een gemiste SignalR-update mag geen dataverlies veroorzaken. De frontend moet bij navigatie, reconnect, frontpageload of berichtenpagina opnieuw de actuele teller/readmodels kunnen ophalen.
De Batch 4-baseline mag zonder actieve SignalR-push sluiten zolang page load, navigatie, openen en expliciet opnieuw ophalen de actuele teller uit IMessageBadgeReader bepalen. Een latere user-scoped MailboxChanged/UnreadBadgeChanged-invalidatie mag hierop aansluiten, maar mag geen berichtinhoud of autorisatiebeslissing naar de client pushen.
13.16 Web-integratie
OefenHub.Web is verantwoordelijk voor routing, weergave, formulieren, componenten en page composition. Web gebruikt de publieke contracts van OefenHub.Communication.
| UI-actie | Technische route |
|---|---|
| Berichtenoverzicht openen | IMailboxReader.GetMailboxAsync |
| Berichtenoverzicht pagineren | OefenHub:Messages:PageSizes, standaard [5, 10, 15]; page size wordt server-side begrensd tot toegestane waarden |
| Mark-read/mark-unread vanuit overzicht | user-scoped mutatie en redirect naar /messages zonder successtatusquery |
| Mailboxitem openen vanuit overzicht | /messages/system/{messageId}/open of /messages/thread/{threadId}/open, daarna redirect naar de canonieke detailroute |
| Bericht openen | IMailboxReader.GetMessageDetailAsync + eventueel markeer-als-gelezen command |
| Privébericht sturen | IPrivateMessageService.SendAsync |
| Thread beantwoorden | IPrivateMessageService.ReplyAsync |
| Thread verwijderen | IPrivateMessageService.DeleteForParticipantAsync |
| Oudere threaditems laden | beveiligde POST met opaque cursor naar de timeline-readservice; geen user-aanpasbare querystring |
| Threadonderwerp wijzigen | threadmenu/modal + server-side participantautorisatie; legt SubjectChanged vast |
| Badge tonen | IMessageBadgeReader.GetBadgeAsync |
| Systeemnotificatie tonen | ISystemNotificationReader.GetActiveNotificationAsync |
Web mag routeparameters zoals threadId, messageId of entityId nooit als autorisatiebewijs gebruiken. Elke command/query herhaalt server-side gebruiker-, rolcontext- en objecttoegangscontrole.
13.17 Autorisatie en objecttoegang
De communicatiemodule controleert bij iedere command en query:
| Controle | Voorbeeld |
|---|---|
| Ingelogde gebruiker | mailbox is altijd user-scoped |
| Ontvanger/participant | gebruiker moet participant of ontvanger zijn |
| Rolcontext indien relevant | beheercontext, oudercontext of docentcontext alleen via server-side context |
| Objecttoegang bij EntityType | ticket, relatie-uitnodiging of gedeelde oefening opnieuw autoriseren bij openen |
| Verwijderstatus | participantgebonden verwijdering respecteren |
| Retentie | verlopen privédata niet tonen |
Een systeembericht geeft geen automatisch recht op het onderliggende object. Het bericht is een ingang naar een vervolgactie, maar die vervolgactie wordt opnieuw door de eigenaarmodule geautoriseerd.
13.18 Retentie en cleanup
Privéberichten hebben een retentiegrens volgens de functionele scope. Systeemberichten blijven langer beschikbaar omdat zij voor gebruikers ook als logging of historie kunnen functioneren, tenzij via een expliciet technisch ontwerpbesluit een expliciet retentiebeleid wordt vastgesteld.
Cleanup wordt niet uitgevoerd door de Web-laag. Periodieke verwerking loopt via OefenHub.Scheduling en roept de communicatiemodule via een publiek contract aan.
| Data | Cleanupbeleid |
|---|---|
| Privéberichten | periodiek op basis van retentiecriteria |
| Participantverwijdering | zichtbaarheid per participant, geen onmiddellijke hard delete |
| Systeemberichten | geen standaard functionele retentiegrens in eerste technische baseline |
| Failed/retry communication actions | begrensd retrybeleid + beheerbare failed-status |
| Browserwaarden voor systeemnotificaties | client-side, geen persoonsgegevens |
Cleanup-acties moeten idempotent zijn. Een gemiste cleanup-run moet via een expliciet technisch ontwerpbesluit veilig opnieuw kunnen draaien.
13.19 Scheduling en retrybare communicatie
Niet-kritieke of uitgestelde communicatieacties kunnen via OefenHub.Scheduling worden verwerkt. De communicatiemodule blijft eigenaar van communicatie-inhoud; Scheduling is eigenaar van de technische joblifecycle.
Voorbeeld:
Support vraagt na ticketupdate een informatief systeembericht aan
- Support bepaalt of bericht kritiek is voor workflow
- Indien niet kritiek: Scheduling krijgt jobaanvraag
- Scheduling bewaakt jobstatus, retries en fouten
- Bij uitvoering roept Scheduling Communication-contract aan
- Communication maakt bericht idempotent aan
Retrybare communicatie moet minimaal beschikken over:
| Onderdeel | Doel |
|---|---|
| CorrelationId | herleidbaarheid naar bronactie |
| IdempotencyKey | voorkomen van dubbele berichten |
| AttemptNumber | retryanalyse |
| LastError | technische foutanalyse |
| Failed-status | beheerbare eindstatus bij blijvend falen |
| TriggeredByModule | herkomst van communicatieverzoek |
Retries zijn begrensd. Een failed communicatieactie mag niet oneindig blijven herhalen.
13.19.1 E-mailafhandeling
E-mail is een ondersteunend transportkanaal en geen functionele bron van waarheid. OefenHub mag e-mail gebruiken voor processen die functioneel zijn vastgelegd, zoals relatie-uitnodigingen naar nog onbekende e-mailadressen of notificatiegerelateerde processen. De technische mailverwerking, mailtemplates, SMTP-dispatch en mailverzendpogingen zijn eigendom van OefenHub.Mail. De bronmutatie blijft eigendom van het domein dat de actie veroorzaakt.
| Situatie | Technisch beleid |
|---|---|
| Identity-providerinterne e-mail, zoals wachtwoordreset of providerverificatie | Buiten OefenHub-domeinlogica; valt onder identity-providerconfiguratie en secrets. |
| Relatie-uitnodiging naar onbekend e-mailadres | Relationships legt de uitnodiging pas vast nadat de gebruiker externe e-mailuitnodiging expliciet bevestigt; e-maildispatch verloopt via OefenHub.Mail wanneer de mail functioneel nodig is om de ontvanger te bereiken. |
| Informatieve notificatiemail | Retrybaar na succesvolle bronmutatie, mits idempotent en begrensd. |
| Mail als primaire ingang tot een workflow | Per workflow bepalen of de mailaanvraag kritisch is en dus atomair met de bronmutatie moet worden vastgelegd. |
| Mailproviderfout bij externe uitnodigingsmail | Geen misleidende succesmelding; voor onbekende adressen mag geen relatie-uitnodiging worden vastgelegd zonder bruikbare mailaanvraag of verzendresultaat volgens de gekozen mailflow. |
Mailinhoud mag geen wachtwoorden, secrets, ruwe tokens, volledige modulepayloads, ongeautoriseerde domeindata of gevoelige technische foutdetails bevatten. Links uit e-mail zijn nooit autorisatiebewijs; vervolgacties controleren opnieuw server-side de actuele account-, rol- en objectcontext.
SMTP-dispatch gebruikt MailKit in plaats van System.Net.Mail.SmtpClient, omdat de OefenHub-SMTP-omgeving implicit SSL/SMTPS op poort 465 kan vereisen. System.Net.Mail.SmtpClient ondersteunt die SSL-on-connect variant niet betrouwbaar; MailKit ondersteunt expliciet zowel SslOnConnect als StartTls. De configuratie blijft twee expliciete booleans gebruiken, vergelijkbaar met Keycloak: Mail:EnableSsl voor implicit SSL en Mail:EnableStartTls voor STARTTLS. Beide mogen niet tegelijk aan staan. Deze transportmodus wordt al tijdens module-/applicatieregistratie gevalideerd, zodat een foutieve combinatie de applicatie direct bij opstarten laat falen en niet pas bij de eerste achtergrondmail zichtbaar wordt.
Voor externe relatie-uitnodigingen is de mailaanvraag onderdeel van de kritieke workflow: de uitnodiging wordt pas zichtbaar/openstaand wanneer de mailtemplate en senderconfiguratie synchroon zijn gevalideerd en wanneer OefenHub.Mail een duurzaam MailSendAttempts-record met geplande deliveryjob heeft vastgelegd. MailSendAttempts bevat generieke bronvelden (SourceEntityType, SourceEntityId, Purpose) zodat een detailsreadmodel veilige deliverysamenvattingen kan ophalen zonder dat de relatie-UI mailtabellen rechtstreeks uitleest. Deliveryjobs controleren vóór SMTP-submit via een bronstatusguard of het bronobject nog geldig is; een ingetrokken, verlopen, geaccepteerde of geweigerde relatie-uitnodiging mag niet alsnog oude uitnodigingsmail versturen.
Maildeliveryhistorie wordt als technische append-only historie vastgelegd. De gebruikersprojectie toont alleen veilige categorieën, zoals E-mail aangeboden aan de mailserver; technische SMTP-, provider- of exceptiondetails blijven voor logging/beheer en worden niet aan gewone gebruikers getoond.
Herinneringen op externe relatie-uitnodigingen maken een nieuwe mailaanvraag aan met Purpose = Reminder. De mailmodule hergebruikt de oorspronkelijke MailSendAttempts-rij niet en bepaalt zelf niet of de gebruiker alweer mag herinneren; die cooldown hoort bij de relatie-/webflow en wordt gelezen uit admin.SystemSettings.RelationshipInvitationReminderCooldownHours. Voor interne ontvangers blijft OefenHub.Communication eigenaar van het nieuwe systeembericht; voor externe ontvangers blijft OefenHub.Mail eigenaar van queueing en delivery.
13.20 Logging en correlation
Alle communicatiecommands en cross-module communicatieverzoeken moeten herleidbaar zijn via correlation-id.
Minimaal te loggen bij mutaties:
| Veld | Toelichting |
|---|---|
CorrelationId | gedeelde trace door workflow heen |
ActorUserId | actor indien gebruikersactie |
ActorRoleContext | rolcontext indien relevant |
RecipientUserId | ontvanger, zonder inhoudelijke berichttekst in technische log |
Action | bijvoorbeeld SystemMessageCreated, PrivateMessageSent |
EntityType | bij systeemberichtverwijzing |
EntityId | bij systeemberichtverwijzing |
Result | success, validation failed, access denied, failed |
Technische logs mogen geen volledige privéberichtinhoud, antwoorddata, tokens, wachtwoorden, sessiecookies of ongefilterde HTML bevatten.
Domeinspecifieke communicatiehistorie blijft in communication-tabellen. Technische logging is bedoeld voor beheer, debugging en securityanalyse, niet als functionele mailboxbron.
13.21 Security- en privacygrenzen
Communicatie bevat potentieel persoonsgegevens en vrije tekst. Daarom gelden extra grenzen:
| Risico | Maatregel |
|---|---|
| XSS via privébericht | server-side sanitizing en veilige encoding |
| Ongeautoriseerde mailboxinzage | user-scoped queries en participantcontrole |
| Routeparameter-manipulatie | server-side objecttoegangscontrole per actie |
| Privéinhoud in logs | inhoud niet in technische logs opnemen |
| Systeembericht naar verkeerd account | ontvanger server-side bepalen, niet clientgestuurd |
| Realtime lek via SignalR group | group membership server-side bepalen |
| Accountanonimisering | persoonsgegevens in zichtbare context volgens anonimiseringbeleid behandelen |
Privéberichtinhoud wordt nooit gebruikt als vrije HTML-output zonder sanitizing en encoding.
13.22 Accountanonimisering
Bij accountanonimisering wordt actieve toegang beëindigd, maar bestaande communicatiehistorie kan functioneel of auditmatig relevant blijven. De communicatiemodule moet anonimisering ondersteunen zonder threads van andere deelnemers onnodig te verwijderen.
| Gegeven | Gedrag bij anonimisering |
|---|---|
| Afzendernaam in oude privéthread | tonen als geanonimiseerde identiteit indien vereist |
| Participantrecords | actieve deelname beëindigen of niet langer autoriserend maken |
| Systeemberichten aan verwijderd account | niet langer regulier toegankelijk voor dat account |
| Threads met andere deelnemers | blijven voor andere deelnemers zichtbaar binnen retentie/zichtbaarheidsregels |
| Technische logs | geen actuele persoonsgegevens tonen buiten beleid |
De exacte anonimiseringswaarden worden centraal vastgelegd in het privacy- en anonimiseringbeleid.
13.23 Templates en beheerbare communicatie-inhoud
Systeemberichttemplates en systeemnotificatie-inhoud kunnen beheerbaar zijn. Technisch moet onderscheid blijven bestaan tussen:
| Type | Eigenaarschap | Opmerking |
|---|---|---|
| Template-inhoud | beheer/configuratie | systeemberichttemplates blijven bij Communication; mailtemplates blijven bij OefenHub.Mail; bestaande templatekeys wijzigen, niet willekeurig nieuwe technische keys aanmaken |
| Template-rendering | Communication voor systeemberichten, OefenHub.Mail voor externe mail | placeholders valideren en veilig renderen |
| Systeemnotificatie-inhoud | beheer/configuratie | geen mailboxitem |
| Popupteksten | popupregister/beheer | geen onderdeel van mailboxcommunicatie |
Templatewijzigingen mogen bestaande historische berichten niet stilzwijgend herschrijven wanneer die berichten als gerenderde tekst zijn opgeslagen. Als berichten op render-time uit templates worden opgebouwd, moet expliciet zijn vastgelegd dat historische weergave kan wijzigen. Voor OefenHub heeft opslag van gerenderde berichttekst bij aanmaak de voorkeur voor historisch consistente mailboxweergave.
13.23.1 Seed- en beheerstrategie voor systeemberichttemplates
Systeemberichttemplates zijn beheerbare communicatie-inhoud, maar geen vrij uitbreidbaar technisch templatingplatform. Iedere template heeft een stabiele TemplateKey die door code en migratie ontstaat. De beheerinterface mag bestaande template-inhoud onderhouden, maar maakt geen nieuwe technische templatekeys aan en wijzigt geen renderer- of placeholdercontracten.
In de database heet deze technische sleutel ReferenceName; in ontwerptekst kan dit conceptueel als templatekey worden aangeduid. De sleutel blijft codevast en read-only.
| Onderdeel | Regel |
|---|---|
TemplateKey | Stabiele technische sleutel; niet beheerbaar en niet afgeleid van de zichtbare titel. |
| Initiële template-inhoud | Wordt alleen aangemaakt wanneer de key ontbreekt. |
| Onderwerp en body | Beheerbaar binnen lengte-, sanitizing- en placeholderregels. |
| Placeholdercontract | Codegedreven allowlist per template; onbekende placeholders worden geweigerd. |
| Renderer | Eigendom van Communication; niet beheerbaar via tekstvelden. |
| Historische berichten | Reeds aangemaakte gerenderde berichten worden niet automatisch herschreven bij templatewijziging. |
| Secrets en tokens | Niet toegestaan in templates, seeddata of gerenderde berichtinhoud. |
Template-seeddata wordt getest als onderdeel van de database- en seedtests: de seed moet herhaalbaar zijn, bestaande beheerwijzigingen behouden en fout geven of beheerreview afdwingen wanneer een technische templatekey of placeholdercontract incompatibel wijzigt.
Voor relatie-uitnodigingen moet Batch 4 bestaande hardcoded systeemberichtteksten vervangen door template-rendering. Minimaal te ondersteunen scenario's zijn: ontvangen interne relatie-uitnodiging, herinnering aan ontvangen relatie-uitnodiging, acceptatiefeedback aan uitnodiger en weigeringfeedback aan uitnodiger. De rendercommand levert alleen placeholders aan die op de allowlist van de gekozen ReferenceName staan. Onbekende placeholders worden geweigerd; ontbrekende verplichte placeholders maken geen half gerenderd bericht aan.
Uitnodigerfeedback gebruikt voor openstaande of oorspronkelijk externe uitnodigingen primair het genormaliseerde uitgenodigde e-mailadres als veilige ontvangerreferentie. De renderer mag niet automatisch de profielnaam van de ontvanger gebruiken tenzij het scenario en privacybeleid dat expliciet toestaan.
13.24 Foutafhandeling
Fouten in communicatie worden veilig afgehandeld zonder gevoelige inhoud te lekken.
| Fout | Gebruikersgedrag | Technische verwerking |
|---|---|---|
| Geen toegang tot bericht | veilige toegang-geweigerdmelding | security/access-denied log zonder inhoud |
| Onderliggend object niet beschikbaar | bericht blijft zichtbaar, actie geblokkeerd | log met EntityType/EntityId |
| Sanitizing faalt | bericht niet opslaan | validatiefout teruggeven |
| Verzenden privébericht faalt | gebruiker krijgt foutfeedback | technische fout met correlation-id |
| Retrybare communicatie faalt blijvend | geen oneindige retry | failed-status + beheeranalyse |
| SignalR update faalt | geen dataverlies | client haalt teller op een volgend moment opnieuw op |
13.25 Voorbeelden
13.25.1 Privébericht sturen
Web ontvangt formulier Nieuw privébericht
1. Web bouwt command DTechnisch Ontwerp uit formulier
2. Web roept IPrivateMessageService.SendAsync aan
3. Communication controleert actor, ontvanger en relatie-/participantcontext
4. Communication sanitizet berichtinhoud
5. Communication maakt of hergebruikt thread
6. Communication slaat message en participant-readstate op
7. Communication commit
8. Web toont resultaat en vraagt badge/mailbox opnieuw op
9. Realtime badge-update wordt na commit verzonden indien beschikbaar
13.25.2 Systeembericht vanuit ticket
Support plaatst externe vraag om aanvullende informatie
1. Support valideert ticketstatus en beheercontext
2. Support slaat externe discussie en statuswijziging op
3. Support bepaalt dat systeembericht nodig is
4. Indien kritiek: Communication maakt bericht binnen workflow boundary
5. Indien niet kritiek: Scheduling verwerkt communicatieverzoek retrybaar
6. Web toont badge pas als gebruiker niet in afleidingsvrije oefencontext zit
13.25.3 Systeemnotificatie na frontpageload
Gebruiker opent frontpage
1. Web laadt frontpage normaal
2. Web vraagt actieve systeemnotificatie op
3. Communication/Admin-configuratie bepaalt zichtbaarheid
4. Web toont notificatie-overlay indien toegestaan
5. Bij actieve leerling-oefenrun wordt overlay uitgesteld
13.26 Teststrategie
Communicatie krijgt module-eigen tests en cross-module integratietests.
| Testtype | Dekking |
|---|---|
| Unit tests | sanitizing, readstate, badge-afleiding, retentieregels |
| Module integration tests | CommunicationDbContext, queries, migrations en seeddata |
| Contract tests | publieke communication-contracts |
| Cross-module tests | relatie-uitnodiging + systeembericht, ticketupdate + communicatie, gedeelde oefening + bericht |
| Security tests | XSS-sanitizing, objecttoegang, participantcontrole |
| Realtime tests | gemiste SignalR update mag geen dataverlies veroorzaken |
| Scheduling tests | retry, idempotency en failed-status voor communicatiejobs |
| Web/component tests | mailboxweergave, badges, verborgen badges tijdens oefenrun |
Architecture tests controleren dat andere modules niet rechtstreeks CommunicationDbContext, communication-entities of interne services gebruiken.
13.27 Implementatiechecklist
Bij implementatie van communicatie moet minimaal worden gecontroleerd:
OefenHub.Communicationheeft éénCommunicationDbContexten schemacommunication.- Communication-entities en implementaties zijn
internalwaar mogelijk. - Publieke interfaces en DTO’s staan onder
Contracts. - Mailboxqueries zijn altijd user-scoped.
- Privéthreadzichtbaarheid en readstate zijn participantgebonden.
- Systeemberichten gebruiken
EntityType+EntityId, geen losse database-URL. - Per workflow is bepaald of het systeembericht kritiek is.
- Rich text wordt server-side gesanitized.
- Badgewaarden zijn afgeleid en niet bronhoudend.
- Badges en notificaties worden tijdens actieve leerling-oefenruns visueel onderdrukt.
- Realtime updates zijn transport en geen bron van waarheid.
- Communicationjobs zijn idempotent en hebben begrensde retries.
- Correlation-id wordt doorgegeven aan logs, jobs en cross-module acties.
- Technische logs bevatten geen volledige privéberichtinhoud of gevoelige payloads.
13.28 Implementatieverificaties
| Punt | Actie |
|---|---|
| Exacte rich text sanitizer | Bepalen welke library of eigen whitelist wordt gebruikt |
| Thread-readstate model | Vastleggen of LastReadMessageId, LastReadAtUtc of combinatie wordt gebruikt |
| SignalR badge-updatekanaal | Open vervolgpunt: user-scoped invalidatie voor nieuwe mailboxitems afstemmen met Web en hoofdstuk 15; Batch 4 gebruikt server-side herberekening bij navigatie/openen |
| Kritieke systeemberichten per workflow | Per workflow expliciet opnemen in de betreffende module-uitwerking |
| Retentie-implementatie privéberichten | Afstemmen met hoofdstuk 18 en privacyhoofdstuk |
| Systeemnotificatie-eigenaarschap | Exacte grens tussen Admin-configuratie en Communication-reader vastleggen |
| Idempotency keys voor communicatiejobs | Sleutelformaat per jobtype bepalen |
| Template-rendering | Keuze maken tussen opslag gerenderde tekst of render-time opbouw per berichttype |