Skip to main content

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:

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.

OnderwerpIn dit hoofdstukBuiten dit hoofdstuk
MailboxweergaveTechnische bron, query-services en readmodelsSchermlabels en exacte UI-copy
SysteemberichtenOpslag, verwijzingen, routinginput en leesstatusFunctionele tekstinhoud per template
PrivéberichtenThreadmodel, participants, rich text sanitizing en retentieVrije chatfunctionaliteit buiten bestaande scope
SysteemnotificatiesTechnische scheiding van mailbox en popupsBeheer-UI voor inhoudelijke notificatieconfiguratie
BadgesAfleiding, realtime update en onderdrukking tijdens oefenrunVisueel ontwerp van badge-iconen
SignalRCommunicatie-updates als transportLive-meekijktransport, dat in hoofdstuk 15 staat
RetentieTechnische verwerking en cleanupJuridisch bewaarbeleid buiten de vastgelegde scope
Cross-module workflowsCommunicatie-ingangen en transaction boundaryDomeinregels van veroorzakende modules
E-mailafhandelingIntegratiepunt richting OefenHub.Mail wanneer interne communicatie een externe mail moet veroorzakenIdentity-providerinterne mailflows zoals wachtwoordreset of provider-e-mailverificatie

13.3 Module-eigenaarschap

De communicatiefuncties worden ondergebracht in het moduleproject OefenHub.Communication.

OnderdeelKeuze
ProjectOefenHub.Communication
DbContextCommunicationDbContext
Database schemacommunication
TabellenPascalCase binnen schema communication
Publieke toegangvia Contracts
Interne implementatieinternal
ReadmodelsModels/ReadModels
Scheduling-integratievia publieke schedulingcontracten
E-mailtransportvia OefenHub.Mail; Communication schrijft niet direct naar SMTP of mailtabellen
Realtime transportvia 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.

DomeinDoelBron van waarheid
SysteemberichtenMailboxitems die door OefenHub of een module worden aangemaaktcommunication.SystemMessages
PrivéthreadsGebruikerscommunicatie met participants en berichtenthread-, participant- en message-tabellen
Thread-eventsSysteemachtige gebeurtenissen binnen een privéthreadthread-eventtabel binnen communication
SysteemnotificatiesSitebrede overlay/notificatie na frontpageloadnotificatieconfiguratie, 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:

RouteDoelAutorisatiebron
/messagesmailboxoverzichtactuele gebruiker + mailboxquery
/messages/system/{messageId}systeemberichtdetailSystemMessages.RecipientUserId
/messages/thread/{threadId}privéthreaddetailPrivateMessageThreadParticipants.UserId + zichtbaarheid
/messages/system/{messageId}/openreadstate eerst bijwerken en daarna naar systeemberichtdetail redirectenSystemMessages.RecipientUserId
/messages/thread/{threadId}/openparticipant-readstate eerst bijwerken en daarna naar threaddetail redirectenPrivateMessageThreadParticipants.UserId + zichtbaarheid
POST /messages/thread/{threadId}/timeline/olderoudere timeline-items zonder volledige page refresh ladenPrivateMessageThreadParticipants.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:

VeldtypeBetekenis
Ontvangerinterne gebruiker voor wie het bericht zichtbaar is
Template of berichttypeverwijzing naar de functionele berichtsoort
Onderwerp/tekstgerenderde of opgeslagen berichtinhoud
EntityTypetype domeinobject waarnaar het bericht verwijst
EntityIdidentifier van het concrete domeinobject
ReadAtUtcleesstatus voor de ontvanger
CreatedAtUtcaanmaakmoment
CorrelationIdtechnische 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:

EntityTypeEigenaarmoduleVoorbeeldgebruik
RelationshipInvitationOefenHub.Relationshipsuitnodiging accepteren of afwijzen
TicketOefenHub.Supportmeldingdetail openen
PrivateMessageThreadOefenHub.Communicationprivéthread openen
SharedExerciseOefenHub.Practiceontvangen 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.

SituatieKritiek?Verwerking
Relatie-uitnodiging waarbij systeembericht de primaire ingang isJaatomair met uitnodiging
Ticketupdate waarbij gebruiker de melding ook via Mijn meldingen kan openenAfhankelijk van flowper workflow bepalen
Gedeelde oefening met aparte ontvangen-overzichtspaginaMogelijk nietretrybaar indien overzicht voldoende is
Badge-updateNeeafgeleid/retrybaar/herbouwbaar
Readmodel-updateNeeafgeleid/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

LaagVerantwoordelijkheid
Threadconversatie-identiteit, onderwerp en lifecycle
Participantgebruiker, deelname, zichtbaarheid en readstate
Messageinhoudelijk privébericht binnen thread
ThreadEventsysteemachtige 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.

ActieTechnisch effect
Thread verwijderen door participanteigen mailboxzichtbaarheid wordt uitgeschakeld
Andere participant opent threadblijft zichtbaar voor die participant
Nieuwe reply nadat een participant heeft verlatenreply wordt geblokkeerd wanneer geen andere actieve deelnemer beschikbaar is; verwijderde participant wordt niet automatisch opnieuw zichtbaar
Retentie verlopencleanup 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:

EventtypeVoorbeeldweergave
SubjectChangedgebruiker wijzigde onderwerp van A naar B
ParticipantLeftgebruiker heeft het gesprek verlaten; actor wordt in de eventtekst opgenomen
ParticipantAddedgebruiker 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.

OnderdeelBeleid
Vet/cursief/onderstreepttoegestaan indien veilig gesanitized
Tekstgroottesbeperkt tot ondersteunde waarden
Opsommingen/genummerde lijstentoegestaan indien veilig gesanitized
Linksalleen indien expliciet toegestaan en veilig gevalideerd
Afbeeldingen/bijlagenbuiten de eerste technische baseline, tenzij via een expliciet technisch ontwerpbesluit apart gespecificeerd
HTML/JavaScriptniet 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:

OnderdeelTechnische grens
Onderwerpmaximaal 120 tekens na trimming en normalisatie.
Zichtbare berichttekstmaximaal 4.000 tekens na sanitizing.
Opgeslagen rich-text bodymaximaal 12 KiB genormaliseerde veilige HTML.
Bijlagenniet 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.

KenmerkSysteemnotificatieSysteemberichtPopupregister-popup
Plaatsboven frontpage na loadmailbox/berichtenoverzichtcontextuele UI-feedback
Persistente mailboxstatusneejanee
ReadAtUtcneejanee
DisplayRulejaneepopupafhankelijk
Beheerbare inhoudjavia templatesja
Doeltijdelijke sitecommunicatiepersoonlijke of objectgerichte communicatieactiefeedback/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.

BadgebronAfleiding
Ongelezen systeemberichtenReadAtUtc ontbreekt voor ontvanger
Ongelezen privéthreadsparticipant-readstate loopt achter op laatste bericht/event
Verwijderde privéthreadstellen niet mee voor betreffende participant
Verlopen/retentie verwijderde privédatatelt 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.

ContractDoel
ISystemMessageServicesysteemberichten aanmaken of markeren binnen toegestane flows
IPrivateMessageServiceprivéberichtthreads starten, beantwoorden, verwijderen en leesstatus verwerken
IMailboxReadermailboxoverzicht en berichtdetails lezen
IMessageBadgeReaderongelezen badges afleiden
ISystemNotificationReaderactieve systeemnotificaties voor context bepalen
ICommunicationTemplateRenderertemplates 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 moduleCommunicatiegebruikEigenaarschap bronactie
Relationshipsuitnodigingsbericht en statusupdatesRelationships
Practicemelding gedeelde oefeningPractice
Supportmelding aangemaakt, info gevraagd, oplossing geplaatstSupport
Cataloginformatie over nieuwe categorie/oefening indien van toepassingCatalog
Adminbeheer van templates, notificaties en contentAdmin of configuratie-eigenaar
LiveMonitoringsysteembericht over meekijken indien functioneel vereistLiveMonitoring

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.

CommunicatiestapTransactiebeleid
Primaire ingang voor vervolgactieatomair met bronactie
Juridisch/functioneel noodzakelijke bevestigingatomair of expliciet beheerbaar falen
Informatieve melding met alternatieve ingangretrybaar toegestaan
Badge-updateafgeleid/retrybaar/herbouwbaar
Realtime updatena 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:

InformatieBron
Ongelezen systeemberichtencommunication.SystemMessages
Ongelezen privéthreadsparticipant-readstate en threadevents
Mailboxlijstmailboxquery/readmodel
Systeemnotificatiezichtbaarheidnotificatieconfiguratie + 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-actieTechnische route
Berichtenoverzicht openenIMailboxReader.GetMailboxAsync
Berichtenoverzicht paginerenOefenHub:Messages:PageSizes, standaard [5, 10, 15]; page size wordt server-side begrensd tot toegestane waarden
Mark-read/mark-unread vanuit overzichtuser-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 openenIMailboxReader.GetMessageDetailAsync + eventueel markeer-als-gelezen command
Privébericht sturenIPrivateMessageService.SendAsync
Thread beantwoordenIPrivateMessageService.ReplyAsync
Thread verwijderenIPrivateMessageService.DeleteForParticipantAsync
Oudere threaditems ladenbeveiligde POST met opaque cursor naar de timeline-readservice; geen user-aanpasbare querystring
Threadonderwerp wijzigenthreadmenu/modal + server-side participantautorisatie; legt SubjectChanged vast
Badge tonenIMessageBadgeReader.GetBadgeAsync
Systeemnotificatie tonenISystemNotificationReader.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:

ControleVoorbeeld
Ingelogde gebruikermailbox is altijd user-scoped
Ontvanger/participantgebruiker moet participant of ontvanger zijn
Rolcontext indien relevantbeheercontext, oudercontext of docentcontext alleen via server-side context
Objecttoegang bij EntityTypeticket, relatie-uitnodiging of gedeelde oefening opnieuw autoriseren bij openen
Verwijderstatusparticipantgebonden verwijdering respecteren
Retentieverlopen 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.

DataCleanupbeleid
Privéberichtenperiodiek op basis van retentiecriteria
Participantverwijderingzichtbaarheid per participant, geen onmiddellijke hard delete
Systeemberichtengeen standaard functionele retentiegrens in eerste technische baseline
Failed/retry communication actionsbegrensd retrybeleid + beheerbare failed-status
Browserwaarden voor systeemnotificatiesclient-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:

OnderdeelDoel
CorrelationIdherleidbaarheid naar bronactie
IdempotencyKeyvoorkomen van dubbele berichten
AttemptNumberretryanalyse
LastErrortechnische foutanalyse
Failed-statusbeheerbare eindstatus bij blijvend falen
TriggeredByModuleherkomst 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.

SituatieTechnisch beleid
Identity-providerinterne e-mail, zoals wachtwoordreset of providerverificatieBuiten OefenHub-domeinlogica; valt onder identity-providerconfiguratie en secrets.
Relatie-uitnodiging naar onbekend e-mailadresRelationships 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 notificatiemailRetrybaar na succesvolle bronmutatie, mits idempotent en begrensd.
Mail als primaire ingang tot een workflowPer workflow bepalen of de mailaanvraag kritisch is en dus atomair met de bronmutatie moet worden vastgelegd.
Mailproviderfout bij externe uitnodigingsmailGeen 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:

VeldToelichting
CorrelationIdgedeelde trace door workflow heen
ActorUserIdactor indien gebruikersactie
ActorRoleContextrolcontext indien relevant
RecipientUserIdontvanger, zonder inhoudelijke berichttekst in technische log
Actionbijvoorbeeld SystemMessageCreated, PrivateMessageSent
EntityTypebij systeemberichtverwijzing
EntityIdbij systeemberichtverwijzing
Resultsuccess, 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:

RisicoMaatregel
XSS via privéberichtserver-side sanitizing en veilige encoding
Ongeautoriseerde mailboxinzageuser-scoped queries en participantcontrole
Routeparameter-manipulatieserver-side objecttoegangscontrole per actie
Privéinhoud in logsinhoud niet in technische logs opnemen
Systeembericht naar verkeerd accountontvanger server-side bepalen, niet clientgestuurd
Realtime lek via SignalR groupgroup membership server-side bepalen
Accountanonimiseringpersoonsgegevens 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.

GegevenGedrag bij anonimisering
Afzendernaam in oude privéthreadtonen als geanonimiseerde identiteit indien vereist
Participantrecordsactieve deelname beëindigen of niet langer autoriserend maken
Systeemberichten aan verwijderd accountniet langer regulier toegankelijk voor dat account
Threads met andere deelnemersblijven voor andere deelnemers zichtbaar binnen retentie/zichtbaarheidsregels
Technische logsgeen 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:

TypeEigenaarschapOpmerking
Template-inhoudbeheer/configuratiesysteemberichttemplates blijven bij Communication; mailtemplates blijven bij OefenHub.Mail; bestaande templatekeys wijzigen, niet willekeurig nieuwe technische keys aanmaken
Template-renderingCommunication voor systeemberichten, OefenHub.Mail voor externe mailplaceholders valideren en veilig renderen
Systeemnotificatie-inhoudbeheer/configuratiegeen mailboxitem
Popuptekstenpopupregister/beheergeen 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.

OnderdeelRegel
TemplateKeyStabiele technische sleutel; niet beheerbaar en niet afgeleid van de zichtbare titel.
Initiële template-inhoudWordt alleen aangemaakt wanneer de key ontbreekt.
Onderwerp en bodyBeheerbaar binnen lengte-, sanitizing- en placeholderregels.
PlaceholdercontractCodegedreven allowlist per template; onbekende placeholders worden geweigerd.
RendererEigendom van Communication; niet beheerbaar via tekstvelden.
Historische berichtenReeds aangemaakte gerenderde berichten worden niet automatisch herschreven bij templatewijziging.
Secrets en tokensNiet 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.

FoutGebruikersgedragTechnische verwerking
Geen toegang tot berichtveilige toegang-geweigerdmeldingsecurity/access-denied log zonder inhoud
Onderliggend object niet beschikbaarbericht blijft zichtbaar, actie geblokkeerdlog met EntityType/EntityId
Sanitizing faaltbericht niet opslaanvalidatiefout teruggeven
Verzenden privébericht faaltgebruiker krijgt foutfeedbacktechnische fout met correlation-id
Retrybare communicatie faalt blijvendgeen oneindige retryfailed-status + beheeranalyse
SignalR update faaltgeen dataverliesclient 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.

TesttypeDekking
Unit testssanitizing, readstate, badge-afleiding, retentieregels
Module integration testsCommunicationDbContext, queries, migrations en seeddata
Contract testspublieke communication-contracts
Cross-module testsrelatie-uitnodiging + systeembericht, ticketupdate + communicatie, gedeelde oefening + bericht
Security testsXSS-sanitizing, objecttoegang, participantcontrole
Realtime testsgemiste SignalR update mag geen dataverlies veroorzaken
Scheduling testsretry, idempotency en failed-status voor communicatiejobs
Web/component testsmailboxweergave, 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.Communication heeft één CommunicationDbContext en schema communication.
  • Communication-entities en implementaties zijn internal waar 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

PuntActie
Exacte rich text sanitizerBepalen welke library of eigen whitelist wordt gebruikt
Thread-readstate modelVastleggen of LastReadMessageId, LastReadAtUtc of combinatie wordt gebruikt
SignalR badge-updatekanaalOpen 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 workflowPer workflow expliciet opnemen in de betreffende module-uitwerking
Retentie-implementatie privéberichtenAfstemmen met hoofdstuk 18 en privacyhoofdstuk
Systeemnotificatie-eigenaarschapExacte grens tussen Admin-configuratie en Communication-reader vastleggen
Idempotency keys voor communicatiejobsSleutelformaat per jobtype bepalen
Template-renderingKeuze maken tussen opslag gerenderde tekst of render-time opbouw per berichttype