Readmodels, tellers, badges, caching en materialisatie
17.1 Doel en afbakening
Dit hoofdstuk beschrijft hoe OefenHub afgeleide weergavegegevens technisch opbouwt, bewaart, ververst en gebruikt. Het gaat om readmodels, tellers, badges, dashboardwaarden, frontpagesamenvattingen, mailboxoverzichten, actie-indicatoren, online-overzichten en andere gegevens die niet de primaire brondata vormen.
De functionele betekenis van tellers en samenvattingen blijft vastgelegd in Functioneel Ontwerp, Software Requirements Specification, schermdocumentatie en database-informatie. Dit hoofdstuk beschrijft de technische realisatie: queryvormen, materialisatie, cachegrenzen, invalidatie, autorisatie, fallbackgedrag, logging en testbaarheid.
Belangrijke uitgangspunten:
- readmodels zijn afgeleide weergave- of querymodellen en geen zelfstandige functionele bron van waarheid;
- tellerwaarden mogen niet impliciet of dubbelzinnig zijn;
- een storing of achterstand in readmodelverversing mag niet stilzwijgend als betrouwbare nulwaarde worden getoond;
- autorisatie wordt altijd opnieuw server-side toegepast, ook wanneer gegevens uit een readmodel of cache komen;
- module-eigenaarschap blijft leidend: readmodels horen primair bij de module die de gegevens bezit of functioneel publiceert;
- er komt in de eerste technische baseline geen centraal
OefenHub.ReadModelsproject.
17.2 Relatie met andere hoofdstukken van het Technisch Ontwerp
| Hoofdstuk | Relatie met dit hoofdstuk |
|---|---|
| Technisch Ontwerp: architectuuroverzicht en solution-opbouw | Legt de modulaire monoliet, projectlijst en module-eigenaarschap vast. |
| Technisch Ontwerp: applicatielagen, projectstructuur en dependency-richting | Legt Contracts, query-services, Models/ReadModels en Web-compositie vast. |
| Technisch Ontwerp: autorisatie, policies en server-side contextcontrole | Bepaalt dat readmodels nooit autorisatie vervangen. |
| Technisch Ontwerp: domeinmodel en datamodel-overzicht | Positioneert readmodels als afgeleide data naast brondata, snapshots, logs en tijdelijke output. |
| Technisch Ontwerp: databaseontwerp, migraties, seeddata en constraints | Beschrijft schema-eigenaarschap, indexes, soft links, snapshots en materialisatietabellen. |
| Technisch Ontwerp: berichten, systeemberichten, notificaties en privéberichten | Bevat mailboxreadmodels, ongelezenbadges en notificatiegedrag. |
| Technisch Ontwerp: realtime live meekijken met SignalR | Gebruikt actuele voortgang en online state; SignalR is transport, geen readmodelbron. |
| Technisch Ontwerp: background jobs, TickerQ en periodieke verwerking | Beschrijft periodieke of retrybare verversing van materialisaties. |
| Technisch Ontwerp: logging, audit, securitylogging en technische foutafhandeling | Legt correlation, foutafhandeling en logging van readmodelverversing vast. |
| Technisch Ontwerp: frontend Blazor, routing, state en componentopbouw | Beschrijft Web-compositie en viewmodels die uit query-services worden opgebouwd. |
17.3 Kernbegrippen
| Begrip | Betekenis binnen OefenHub | Bron van waarheid? |
|---|---|---|
| Brondata | Primaire domeintabellen zoals ExerciseRuns, SystemMessages, Tickets, UserRelationships of Levels. | Ja |
| Snapshot | Historische vastlegging van context zoals naam, rol, categorie of moduleversie op een functioneel moment. | Ja, voor die historische context |
| Readmodel | Afgeleid model voor snelle of consistente weergave van gegevens. | Nee |
| Teller | Afgeleide numerieke waarde op basis van expliciete scope en filters. | Nee |
| Badge | Visuele indicator van een teller of actiebehoefte. | Nee |
| UI-viewmodel | Model dat OefenHub.Web gebruikt voor pagina- of componentweergave. | Nee |
| Cache | Tijdelijke opslag om herhaalde berekening of query’s te verminderen. | Nee |
| Materialisatie | Persistent opgeslagen afgeleide waarde of projectie. | Nee, tenzij expliciet als brondata gemodelleerd |
Een readmodel mag functioneel belangrijk zijn voor gebruikerservaring en performance, maar blijft technisch herbouwbaar of verifieerbaar uit brondata. Wanneer een readmodel niet herbouwbaar is zonder informatieverlies, is het waarschijnlijk geen readmodel maar brondata of snapshotdata en hoort het in database-informatie als zodanig te zijn gemodelleerd.
17.4 Eigenaarschap van readmodels
OefenHub gebruikt geen centraal OefenHub.ReadModels project in de eerste technische baseline. Readmodels worden geplaatst bij de module die eigenaar is van de brondata of van de functionele publicatie.
| Readmodelcategorie | Eigenaar | Locatie in project |
|---|---|---|
| Oefenrun- en resultaatreadmodels | OefenHub.Practice | Models/ReadModels |
| Mailbox-, thread- en badge-readmodels | OefenHub.Communication | Models/ReadModels |
| Ticketoverzicht en actie-indicatoren | OefenHub.Support | Models/ReadModels |
| Catalogusoverzichten | OefenHub.Catalog | Models/ReadModels |
| Livebeschikbaarheid en online-overzicht | OefenHub.LiveMonitoring | Models/ReadModels |
| Beheerfrontpage- en adminoverzichten | OefenHub.Admin of eigenaarmodule | Models/ReadModels |
| Exportmodellen | OefenHub.Reporting, gevoed via broncontracts | Models/ReadModels of Models/ExportModels |
| UI-compositie over meerdere modules | OefenHub.Web | PageComposition en ViewModels |
17.4.1 Geen centraal readmodelproject
Een centraal readmodelproject wordt in de basis vermeden omdat dit project anders snel meerdere domeinen rechtstreeks met elkaar zou koppelen. Samengestelde pagina’s worden daarom in OefenHub.Web opgebouwd via publieke query-services van de betrokken modules.
17.4.2 Expliciet benoemde readmodel- en eventnamen
De ontwerpbronnen en usecases benoemen een aantal ouder-/voogd- en beheerreadmodels bij naam. Deze namen worden in het Technisch Ontwerp niet als aparte brondata behandeld, maar als publieke readmodel-, exportmodel- of eventcontracten van de eigenaarmodule.
| Naam | Technische eigenaar | Type | Technische betekenis |
|---|---|---|---|
AdminFrontpageReadModel | OefenHub.Admin | Readmodel | Afgeleid beheerfrontpageoverzicht op basis van beheerbare content, recente beheerwijzigingen en relevante beheerindicatoren. |
GuardianChildReadModel | OefenHub.Relationships, eventueel gecomposeerd in OefenHub.Web | Readmodel | Afgeleid overzicht van actief gekoppelde kinderen op basis van actieve GuardianStudent-relaties. |
GuardianResultSummaryReadModel | OefenHub.Practice | Readmodel | Compacte resultatensamenvatting per gekoppeld kind op basis van afgeronde, niet-test runs. |
GuardianHistoryReadModel | OefenHub.Practice | Readmodel | Geschiedenisregels van afgeronde runs binnen de toegestane kinddataset. |
GuardianHistoryFilterReadModel | OefenHub.Practice | Readmodel | Filteropties en gefilterde geschiedenisset voor periode, niveau, categorie en oefening, uitsluitend opgebouwd uit toegestane runs. |
GuardianRunDetailReadModel | OefenHub.Practice | Readmodel | Read-only detailweergave van één geautoriseerde afgeronde run met historische context en uniforme totalen. |
GuardianResultStatisticsReadModel | OefenHub.Practice | Readmodel | Detail- en statistiekweergave op basis van opgeslagen runvelden en vraagvoortgang, zonder clientside herberekening. |
GuardianPdfExportModel | OefenHub.Reporting, gevoed door OefenHub.Practice | Exportmodel | Tijdelijk exportmodel voor PDF-download van een geautoriseerde afgeronde run; geen permanent documentrecord. |
GuardianOnlineOverviewReadModel | OefenHub.LiveMonitoring | Readmodel | Online- en oefenstatusoverzicht van actief gekoppelde kinderen. Het openen van het overzicht maakt geen LiveViewAudit aan. |
LiveAvailabilityReadModel | OefenHub.LiveMonitoring | Readmodel | Afgeleide beschikbaarheid van de actie Kijk live mee; de knopstatus is geen autorisatiebron. |
GuardianAccessDenied | OefenHub.Authorization en technische logging volgens hoofdstuk 19 | Security-/access-denied-event | Beperkte registratie van geweigerde ouder-/voogdtoegang zonder resultaatinhoud, antwoorden, tokens of gevoelige payloads. |
Voor alle genoemde readmodels geldt dat de actuele server-side autorisatie opnieuw wordt toegepast bij iedere query. Een route-id, filterwaarde of eerder getoonde schermstatus mag geen toegang verruimen.
Voorbeeld:
var catalogSummary = await catalogQueries.GetTeacherCatalogSummaryAsync(context, cancellationToken);
var practiceSummary = await practiceQueries.GetTeacherPracticeSummaryAsync(context, cancellationToken);
var supportIndicator = await supportQueries.GetActionIndicatorAsync(context, cancellationToken);
var viewModel = TeacherFrontpageViewModel.Compose(
catalogSummary,
practiceSummary,
supportIndicator);
OefenHub.Web mag deze gegevens combineren tot een UI-viewmodel, maar mag geen directe databasequery uitvoeren op module-interne tabellen.
17.4.3 Uitzondering via expliciet ontwerpbesluit
Een apart compositie- of readmodelproject mag via een expliciet besluit worden toegevoegd wanneer er een duidelijke technische noodzaak ontstaat, bijvoorbeeld:
- persistente domeinoverstijgende projecties met eigen lifecycle;
- complexe rapportageprojecties die niet logisch bij één module horen;
- schaal- of performance-eisen die Web-compositie ongeschikt maken;
- hergebruik van dezelfde samengestelde projecties door meerdere niet-Web-ingangen.
Zo’n project mag niet stilzwijgend ontstaan. Het vereist een expliciet besluit in het Technisch Ontwerp en moet een eigen eigenaarschap, dependency-richting en datatoegangsbeleid krijgen.
17.5 Query-services en readmodel-readers
Modules publiceren readmodels via publieke contracten. Andere modules of Web gebruiken niet de interne repositories, DbContexts of entities van de eigenaarmodule.
Voorbeelden van publieke leescontracten:
public interface IExerciseRunResultReader
{
Task<ExerciseRunResultReadModel> GetResultAsync(
ExerciseRunResultQuery query,
CancellationToken cancellationToken);
}
public interface IMessageBadgeReader
{
Task<MessageBadgeReadModel> GetBadgeAsync(
MessageBadgeQuery query,
CancellationToken cancellationToken);
}
public interface ITicketActionIndicatorReader
{
Task<TicketActionIndicatorReadModel> GetIndicatorAsync(
TicketActionIndicatorQuery query,
CancellationToken cancellationToken);
}
Regels:
- query-services staan als publieke interface onder
Contracts; - contract-DTO’s en publieke readmodels staan onder
Contracts/Modelsof worden expliciet publiek gemaakt vanuitModels/ReadModelswanneer dat de gekozen moduleconventie is; - interne projecties, EF-querytypes en helpermodellen blijven
internal; - readmodel-readers voeren zelf server-side scoping en autorisatievoorbereiding uit of vragen daarvoor expliciet
OefenHub.Authorizationaan; - een reader geeft geen records terug buiten de geautoriseerde dataset.
17.6 Tellerdefinities en scope
Iedere teller moet expliciet definiëren wat wordt geteld. Een teller zonder scopeomschrijving is technisch niet toegestaan.
Minimale tellerdefinitie:
| Eigenschap | Verplicht | Toelichting |
|---|---|---|
| Naam | Ja | Functionele en technische naam van de teller. |
| Eigenaarmodule | Ja | Module die de teller publiceert. |
| Brondata | Ja | Tabellen, snapshots of readmodelbron. |
| Actor-/rolcontext | Ja | Voor welke gebruiker en rolcontext de teller geldt. |
| Objectscope | Ja | Bijvoorbeeld kind, leerling, docent, niveau, categorie of ticket. |
| Statusfilters | Ja | Welke statussen tellen mee of worden uitgesloten. |
| Tijdvenster | Indien van toepassing | Bijvoorbeeld week, maand, jaar of alles. |
| Testdata uitgesloten | Indien van toepassing | Bijvoorbeeld docenttestruns uitsluiten. |
| Soft-deleted/inactieve records | Ja | Expliciet meenemen of uitsluiten. |
| Actualiteitseis | Ja | Live, near-realtime, bij paginalaad, periodiek of eventual. |
| Fallbackgedrag | Ja | Wat gebeurt bij fout of stale materialisatie. |
Voorbeelddefinitie:
| Teller | Eigenaar | Definitie |
|---|---|---|
| Ongelezen berichten | Communication | Aantal voor de gebruiker zichtbare systeemberichten zonder ReadAtUtc plus privéthreads met thread-events of berichten na de participant-readstate, exclusief participant-verwijderde threads. |
| Meldingen wacht op mij | Support | Aantal eigen tickets met backendstatus WaitingForUser, tenzij de gebruiker geen actieve OefenHub-sessie of geen toegang meer heeft tot het account. |
| Afgeronde oefeningen kind | Practice | Aantal afgeronde, niet-test exercise runs van een actief gekoppeld kind binnen de geautoriseerde ouder-/voogdrelatie. |
| Populaire categorie leerling | Practice of Catalog in samenwerking | Aantal afgeronde runs binnen het actieve leerlingniveau en toegankelijke categorieën, scoped door actuele server-side autorisatie. |
Een teller mag in de UI niet worden hergebruikt voor een andere betekenis zonder aparte definitie.
17.7 Badgebeleid
Badges zijn visuele indicatoren bovenop tellers of actiebehoefte. De badge zelf is geen brondata.
| Badge | Eigenaar | Bron | Bijzonder gedrag |
|---|---|---|---|
| Berichtenbadge | Communication | Ongelezen berichten en thread-events | Tijdens actieve leerling-oefening visueel onderdrukken. |
| Meldingenactie-indicator | Support | Tickets met actie voor gebruiker | Tijdens actieve leerling-oefening visueel onderdrukken. |
| Beheerattentie | Admin of eigenaarmodule | Beheerreadmodels | Alleen in beheercontext zichtbaar. |
| Online/live-beschikbaarheid | LiveMonitoring | Online presence en actieve runstatus | Knopstatus is geen autorisatiebewijs. |
| Relatie-uitnodiging | Relationships via Communication | Systeembericht of relationship-readmodel | Acceptatie vereist server-side hercontrole. |
17.7.1 Afleidingsvrije oefencontext
Tijdens een actieve leerling-oefenrun worden nieuwe badges, meldingsindicaties en systeemnotificatie-overlays niet zichtbaar gemaakt in de leerling-UI. De onderliggende brondata en readstates blijven wel correct opgeslagen.
Technisch betekent dit:
CommunicationenSupportmogen server-side tellerwaarden blijven actualiseren;OefenHub.Webonderdrukt zichtbare badgeverversing binnen de actieve oefenlayout;- SignalR-updates mogen binnenkomen maar worden niet als afleidende UI-indicatie getoond;
- na verlaten, onderbreken of afronden van de oefencontext worden badges opnieuw opgehaald via de publieke readers;
- de tijdelijke UI-onderdrukking wijzigt geen readstate, berichtstatus, ticketstatus of oefenvoortgang.
17.8 Querymatig versus gematerialiseerd
Niet elk readmodel hoeft gematerialiseerd te worden. De keuze wordt per readmodel gemaakt.
| Variant | Gebruik | Voordeel | Nadeel |
|---|---|---|---|
| Directe query | Kleine datasets, simpele filters, lage frequentie | Altijd actueel, weinig extra opslag | Kan duur worden bij dashboards of veel gebruikers. |
| Geoptimaliseerde query-service | Samengestelde query binnen één module | Modulegrens blijft intact | Nog steeds runtimebelasting. |
| Database view/querytype | Leesoptimalisatie binnen schema | Duidelijke SQL-projectie | Minder flexibel bij complexe autorisatie. |
| Materialisatietabel | Tellers, badges, dashboards, zware overzichten | Snel en stabiel in UI | Vereist invalidatie, rebuild en foutafhandeling. |
| In-memory cache | Stabiele referentie- of configuratiedata | Snel, eenvoudig | Proceslokaal en niet bronhoudend. |
| Web viewmodel | Pagina-/componentcompositie | UI-specifiek en flexibel | Niet herbruikbaar als bron. |
Keuzeregel:
Begin met query-services zolang performance, eenvoud en consistentie voldoen. Materialiseer pas wanneer een teller, dashboard of overzicht aantoonbaar te duur, te complex of te vaak nodig is om iedere keer live te berekenen.
Uitzondering: wanneer een functionele eis expliciet een stabiel historisch snapshot vereist, wordt dat niet als readmodel maar als snapshotbrondata gemodelleerd.
17.9 Materialisatiebeleid
Wanneer een readmodel persistent wordt opgeslagen, gelden de volgende regels:
- de materialisatietabel staat in het schema van de eigenaarmodule;
- de eigenaarmodule beheert migrations, indexes en rebuildlogica;
- de materialisatie bevat metadata over actualiteit, bijvoorbeeld
CalculatedAtUtcofSourceVersionwanneer nodig; - de materialisatie mag niet worden bijgewerkt door andere modules;
- andere modules gebruiken publieke query-services;
- rebuilds zijn idempotent;
- falende rebuilds zijn herleidbaar via logging en correlation;
- bij mismatch tussen brondata en materialisatie heeft brondata voorrang.
Voorbeelden:
| Materialisatie | Schema | Eigenaar | Mogelijke verversing |
|---|---|---|---|
| Mailboxoverzicht | communication | Communication | Bij berichtmutatie of periodieke hersteljob. |
| Ongelezen badge | communication | Communication | Bij readstate-/threadmutatie. |
| Ticketactie-indicator | support | Support | Bij ticketstatus- of discussie-mutatie. |
| Guardian-resultaatsamenvatting | practice | Practice | Bij afronden van run, relatiecontrole bij query. |
| Beheerfrontpage-attentieblok | admin of eigenaarmodule | Afhankelijk van bron | Periodiek of querymatig. |
| Online-overzicht | live | LiveMonitoring | Runtime/readmodel, beperkt persistent. |
17.9.1 Initiële kandidaten voor materialisatie
De ontwerpbronnen, schermdocumentatie, usecases en reeds benoemde readmodelnamen geven voldoende informatie om een initiële materialisatiebaseline te bepalen. Deze baseline sluit niet uit dat een implementatiestory later op basis van meetgegevens een optimalisatie toevoegt of terugdraait, maar voorkomt dat ieder readmodel opnieuw als open architectuurvraag wordt behandeld.
De onderstaande indeling is de V1.0-startpositie:
| Readmodel of projectie | Eigenaar | Baselinevorm | Reden | Verversing of herstel |
|---|---|---|---|---|
| Mailboxoverzicht per gebruiker | OefenHub.Communication | Fysiek gematerialiseerde projectie | Veelgebruikte lijst en badgebasis, afhankelijk van berichten, threads en participant-readstate. | Bij bericht-, thread- en readstate-mutatie; periodieke rebuild via Scheduling mogelijk. |
| Ongelezen berichtenbadge | OefenHub.Communication | Fysiek gematerialiseerde teller of afgeleide badgeprojectie | Veel zichtbaar, contextgevoelig en vaak nodig in de paginaschil. | Bij readstate-/threadmutatie; idempotente rebuild vanuit brondata. |
| Ticketactie-indicator | OefenHub.Support | Fysiek gematerialiseerde teller of actieprojectie | Gebruikt voor meldingen, tickets en aandachtspunten per gebruiker/rolcontext. | Bij ticketstatus-, discussie- en eigenaarmutaties; hersteljob mogelijk. |
GuardianResultSummaryReadModel | OefenHub.Practice | Kandidaat voor fysieke materialisatie per leerling/kindscope | Resultaatsamenvattingen worden herhaald gebruikt op ouder-/voogdfrontpages en kindoverzichten. | Bij afronden, corrigeren of verwijderen/anonimiseren van runs; relatieautorisatie blijft query-time. |
AdminFrontpageReadModel | OefenHub.Admin of eigenaarmodule per blok | Gemengde materialisatie: zware attentie- en aggregatieblokken materialiseren, kleine configuratie live queryen | Beheerfrontpage combineert meerdere beheerindicatoren; niet ieder blok heeft dezelfde actualiteitseis. | Periodiek, bij beheerwijziging of via module-eigen rebuild; blokken blijven afzonderlijk herstelbaar. |
| Beheerattentieblokken voor content, features, modules en support | OefenHub.Admin, OefenHub.Content, OefenHub.Catalog, OefenHub.Support | Kandidaat voor fysieke materialisatie per eigenaarmodule | Voorkomt dure beheerfrontpagequeries over meerdere domeinen. | Bij bronmutatie of periodieke refresh; Web composeert alleen via publieke readers. |
| Catalogusoverzichten voor docent/leerling | OefenHub.Catalog | Query-service met eventuele cache; materialisatie alleen bij meetbare noodzaak | Catalogusdata is relatief stabiel, maar autorisatie en beschikbaarheid kunnen contextgevoelig zijn. | Cache-invalidatie bij categorie-, niveau-, module- of featurewijziging. |
GuardianHistoryReadModel | OefenHub.Practice | Geïndexeerde query-service met paging, geen initiële fysieke materialisatie | Geschiedenis is filter- en periodegevoelig; brondata en snapshots blijven leidend. | Query-time op Practice-brondata met server-side autorisatie. |
GuardianHistoryFilterReadModel | OefenHub.Practice | Query-service of korte cache, geen initiële fysieke materialisatie | Filteropties moeten aansluiten op de actuele geautoriseerde dataset. | Verversen bij paginalaad of korte contextveilige cache. |
GuardianRunDetailReadModel | OefenHub.Practice | Directe read-only query, geen fysieke materialisatie | Eén afgeronde run met opgeslagen snapshots en resultaatvelden is de bron. | Query-time met server-side autorisatie. |
GuardianResultStatisticsReadModel | OefenHub.Practice | Query-service; alleen afgeleide zware statistieken materialiseren wanneer meetbaar nodig | Statistieken moeten reproduceerbaar zijn uit opgeslagen run- en vraagdata. | Bij runafronding of query-time; eventuele materialisatie per run herbouwbaar. |
GuardianPdfExportModel | OefenHub.Reporting, gevoed door OefenHub.Practice | Tijdelijk exportmodel, geen materialized readmodel | PDF-output is tijdelijke output en geen permanente bron of dashboardprojectie. | Op aanvraag genereren; cleanup volgens hoofdstuk 16 en 18. |
GuardianOnlineOverviewReadModel | OefenHub.LiveMonitoring | Near-realtime runtime/readmodel met korte contextveilige cache; geen duurzame materialisatietabel als baseline | Online state en oefenstatus wijzigen snel en zijn SignalR-/sessiegevoelig. | Bij presence-/runstatusupdate en reconnect; fallback naar actuele bronstatus. |
LiveAvailabilityReadModel | OefenHub.LiveMonitoring | Runtime/readmodel of korte cache, geen duurzame materialisatietabel als baseline | Knopbeschikbaarheid is afgeleid en mag nooit autorisatie vervangen. | Query-time hercontrole bij openen of starten van live meekijken. |
Voor alle kandidaten gelden de algemene regels uit dit hoofdstuk: de materialisatie is geen bron van waarheid, wordt eigenaarmodule-specifiek beheerd, is herbouwbaar of expliciet herstelbaar, en wordt nooit gebruikt om server-side autorisatie te vervangen.
De volgende onderdelen blijven implementatieverificatie per module, maar zijn geen open architectuurvraag meer:
- concrete tabel- of viewnamen voor materialisaties;
- indexdefinities en queryplannen;
- exacte TTL's voor caches;
- rebuildfrequenties en retrydefaults;
- concrete performancegrenzen waarbij een query alsnog wordt gematerialiseerd.
17.10 Invalidatie en verversing
Invalidatie wordt zoveel mogelijk gestuurd vanuit de module die de bronmutatie uitvoert.
17.10.1 Binnen module
Binnen één module mag de bronmutatie direct de bijbehorende materialisatie bijwerken wanneer dit onderdeel is van dezelfde functionele consistentiegrens.
Voorbeeld:
Communication
- privébericht opslaan
- participant-readstate bepalen
- mailboxreadmodel bijwerken
- ongelezenbadge bijwerken
Als deze afgeleide waarden essentieel zijn voor de mailboxweergave en in hetzelfde schema blijven, kan dit binnen dezelfde moduletransactie gebeuren.
17.10.2 Over modulegrens
Wanneer een mutatie in module A een readmodel in module B beïnvloedt, wordt dit niet rechtstreeks via databasewrites opgelost. Mogelijke routes:
| Route | Gebruik |
|---|---|
| Publiek contract | Module A vraagt module B om een eigen readmodel te verversen. |
| Application event | Module A publiceert een gebeurtenis binnen de applicatie; module B verwerkt deze. |
| Scheduling job | Verversing mag uitgesteld of retrybaar plaatsvinden. |
| Live query bij ophalen | Geen materialisatie; module B vraagt bronstatus via contract. |
| Rebuild/hersteljob | Periodieke correctie van materialisaties. |
Cross-module invalidatie mag geen directe update uitvoeren in tabellen van een andere module.
17.10.3 Rebuildstrategie
Materialisaties moeten herbouwbaar zijn uit brondata, tenzij expliciet anders gemotiveerd. Rebuild kan nodig zijn bij:
- foutieve eerdere verwerking;
- gewijzigde tellerdefinitie;
- herstel na jobstoring;
- deployment/migratie;
- datacorrectie door beheer;
- performance- of indexwijzigingen.
Rebuilds draaien via module-eigen services. OefenHub.Scheduling kan de uitvoering plannen en bewaken, maar wordt geen eigenaar van de domeinlogica.
17.11 Cachinglagen
OefenHub onderscheidt meerdere cachinglagen. Geen enkele cache mag autorisatie of brondata vervangen.
| Cachelaag | Voorbeeld | Toegestaan voor | Niet toegestaan voor |
|---|---|---|---|
| Request-scope cache | Hergebruik van context binnen één request | Rolcontext, herhaalde queryresultaten | Persistente beslissingen |
| In-memory app-cache | Modulemetadata, stabiele configuratie | Korte performance-optimalisatie | Brondata, autorisatie-uitkomst zonder hercontrole |
| Browserstate | Toegankelijkheidswaarde vóór login, UI-selectie | Presentatiegedrag | Autorisatie, persoonsgegevens, rolcontext |
| SignalR-clientstate | Laatste ontvangen live-update | Weergave tijdens verbinding | Bron van oefenvoortgang |
| Materialisatietabel | Badge/teller/readmodel | Snelle server-side query | Enige bron van waarheid |
17.11.1 Cache invalidatie
Caches krijgen altijd een duidelijke invalidatiestrategie:
- tijdgebaseerde expiratie;
- expliciete invalidatie na mutatie;
- verversing bij paginalaad;
- verversing na SignalR-reconnect;
- rebuild via Scheduling;
- fallback naar bronquery wanneer betrouwbaarheid vereist is.
17.11.2 Caching en autorisatie
Autorisatiebeslissingen mogen niet onbeperkt worden gecachet. Wanneer een cache geautoriseerde data bevat, moet minimaal de volgende scope onderdeel zijn van de cache key of de server-side query:
UserIdof actor-id;- actieve rolcontext;
- relevante relatiecontext;
- relevante objectscope zoals kind, leerling, niveau of ticket;
- featurestatus wanneer relevant;
- eventueel tenant-/omgevingcontext wanneer toekomstig toegevoegd.
17.12 Autorisatie bij readmodels
Een readmodel mag alleen gegevens bevatten of teruggeven die binnen de actuele server-side context zichtbaar mogen zijn. Dit geldt ook als het readmodel technisch al voor meerdere gebruikers of objecten is voorgeselecteerd.
Regels:
- query-services valideren actor, rolcontext en objectscope;
- routeparameters of clientfilters zijn nooit autoriserend;
- readmodels voor ouder-/voogdresultaten hercontroleren de actieve GuardianStudent-relatie;
- docentreadmodels hercontroleren docentcontext, niveauautorisatie en leerlingrelatie;
- beheerreadmodels vereisen actieve beheercontext;
- badges voor verborgen of ontoegankelijke objecten worden niet getoond;
- bij ontbrekende toegang wordt geen gedeeltelijke resultaatinhoud teruggegeven.
Voorbeeld:
GuardianResultSummaryReadModel
- mag intern gebaseerd zijn op afgeronde runs van een kind;
- wordt alleen teruggegeven als de actuele gebruiker op dat moment een actieve GuardianStudent-relatie heeft;
- filterwaarden uit de browser verruimen de dataset nooit.
17.13 Privacy en persoonsgegevens
Readmodels mogen geen onnodige persoonsgegevens dupliceren. Wanneer persoonsgegevens nodig zijn voor weergave, wordt per readmodel bepaald of live uitlezen, snapshot of geanonimiseerde fallback nodig is.
| Gegeven | Voorkeur |
|---|---|
| Actuele naamweergave in actieve relaties | Via eigenaarmodule of expliciet readmodel. |
| Historische naam bij run/export | Snapshot vanuit Practice. |
| Geanonimiseerde gebruiker | Vooraf gedefinieerde anonimiseringswaarde. |
| Technische identifiers | Niet tonen aan eindgebruiker tenzij functioneel nodig. |
| Zoekvelden met persoonsgegevens | Alleen waar functioneel nodig, server-side gescoped. |
Bij accountanonimisering moeten readmodels die persoonsgegevens bevatten worden:
- opnieuw opgebouwd;
- geanonimiseerd via eigenaarmodule;
- of ongeldig verklaard en bij volgende query opnieuw berekend.
Dit beleid wordt per module uitgewerkt waar persoonsgegevens in readmodels worden opgeslagen.
17.14 Fallbackgedrag
Een readmodelstoring mag niet leiden tot misleidende waarden.
| Situatie | Fallback |
|---|---|
| Materialisatie ontbreekt maar bronquery is goedkoop | Bronquery uitvoeren en eventueel materialisatie herstellen. |
| Materialisatie ontbreekt en bronquery is duur | Niet-beschikbaarmelding of beheerbare herstelstatus tonen. |
| Badge kan niet betrouwbaar worden berekend | Badge verbergen of neutrale foutstatus tonen; geen 0 alsof er niets is. |
| Readmodel is stale maar nog acceptabel | Waarde tonen met server-side bekende actualiteit indien relevant. |
| Readmodel is stale en actiegevoelig | Brondata controleren of actie blokkeren tot hercontrole. |
| Rebuild faalt | Failed-status loggen met correlation en beheerbaar herstelpunt. |
Voor gebruikersgerichte schermen geldt:
- geen technische foutdetails tonen;
- geen gevoelige objectnamen lekken bij autorisatiefouten;
- lege toestand en fouttoestand duidelijk scheiden;
- bij twijfel geen inhoudelijke data tonen.
17.15 Realtime en near-realtime waarden
Niet alle waarden hoeven realtime te zijn. Per readmodel wordt een actualiteitsklasse gekozen.
| Klasse | Betekenis | Voorbeelden |
|---|---|---|
| Direct consistent | Waarde moet direct kloppen bij commandresultaat. | Vraagvoortgang na antwoord, kritieke status in ticketflow. |
| Near-realtime | Waarde mag kort achterlopen maar wordt actief bijgewerkt. | Berichtenbadge, live-overzicht, online state. |
| Bij paginalaad actueel | Waarde wordt bij openen van scherm opnieuw opgehaald. | Geschiedenisfilters, frontpage-samenvatting. |
| Periodiek | Waarde mag via job worden ververst. | Beheerattentie, cleanup-indicatoren, zware aggregaties. |
| Herbouwbaar | Waarde is vooral performanceoptimalisatie. | Dashboardprojecties, rapportagevoorbereiding. |
SignalR mag worden gebruikt om clients op een nieuwe waarde te wijzen, maar de client moet zo nodig de actuele waarde opnieuw via een server-side reader ophalen. SignalR-payloads zijn geen autorisatiebewijs en geen primaire brondata.
17.16 Tellers per domein
17.16.1 Practice
OefenHub.Practice publiceert readmodels voor:
- afgeronde runs;
- geschiedenisoverzichten;
- resultaatdetails;
- statistiekweergaven;
- ouder-/voogdresultaatsamenvattingen;
- docentresultaatinzage;
- gedeelde-oefeningsoverzichten;
- voortgangssamenvattingen voor live meekijken.
Brondata bestaat uit practice-tabellen zoals exercise runs, voortgang, snapshots en shared exercise records. Niet-afgeronde runs en docenttestruns worden uitgesloten waar de functionele teller dat vereist.
17.16.2 Communication
OefenHub.Communication publiceert readmodels voor:
- mailboxoverzicht;
- systeemberichten;
- privéthreads;
- participant-readstate;
- ongelezenbadges;
- systeemnotificaties.
Participantgebonden zichtbaarheid is leidend. Verwijdering van een privéthread door één deelnemer mag geen readmodelimpact hebben voor andere deelnemers.
17.16.3 Support
OefenHub.Support publiceert readmodels voor:
- mijn meldingen;
- beheerdersoverzicht;
- actie-indicatoren;
- tickets wachtend op gebruiker;
- recente ticketactiviteit;
- detailweergaven met externe of interne zichtbaarheid.
Gebruikersreadmodels tonen uitsluitend eigen meldingen en externe communicatie. Beheerreadmodels vereisen actieve beheercontext.
17.16.4 Catalog
OefenHub.Catalog publiceert readmodels voor:
- niveaulijsten;
- categorieën per niveau;
- oefeningen per categorie;
- modulebeschikbaarheid;
- oefeningstatussen;
- docent-oefenaanbod;
- beheerimpact bij categorie- en modulemigraties.
Catalogusreadmodels tonen geen leerlingresultaten en gebruiken Practice niet als bron, behalve via expliciete querycontracten voor samenvattende telwaarden wanneer dat nodig is.
17.16.5 LiveMonitoring
OefenHub.LiveMonitoring publiceert readmodels voor:
- online-overzichten;
- livebeschikbaarheid;
- actieve live-sessies;
- auditstatus van live meekijken.
Online-status is runtime/readmodelgedrag en geen autorisatiebron. Starten van live meekijken hercontroleert altijd de actuele relatie- en oefencontext.
17.16.6 Admin
OefenHub.Admin publiceert beheerreadmodels voor:
- beheerfrontpage;
- contentbeheer;
- recente beheerwijzigingen;
- systeeminstellingen;
- popup- en templatebeheer;
- module- en categorie-impactoverzichten.
Waar deze waarden uit andere domeinen komen, gebruikt Admin publieke query-services en geen directe databasekoppelingen.
17.17 Indexen en queryperformance
Readmodel- en tellerquery’s moeten worden ondersteund door passende indexen in het schema van de eigenaarmodule. Indexkeuzes worden in database-informatie en hoofdstuk 07 verder geconcretiseerd wanneer tabeldefinities worden aangepast.
Algemene indexrichtlijnen:
- indexeer actor-/ownerkolommen die vaak in server-side scoping worden gebruikt;
- indexeer statuskolommen die onderdeel zijn van tellerdefinities;
- indexeer
CreatedAtUtc,CompletedAtUtc,UpdatedAtUtcof vergelijkbare tijdvelden wanneer filters op periode bestaan; - gebruik samengestelde indexen voor veelgebruikte combinatiequeries;
- voorkom brede indexes op grote JSON/base64-payloads;
- gebruik opgeslagen uniforme kolommen voor geschiedenis en statistiekfilters;
- voorkom query’s die autorisatie pas achteraf in memory toepassen.
Voorbeeld:
practice.ExerciseRuns
- StudentUserId
- LevelId
- ExerciseId
- CompletedAtUtc
- IsCompleted
- IsTestRun
Een geschiedenisquery voor afgeronde leerlingruns moet server-side kunnen filteren op student, context, completion status en periode zonder eerst alle runpayloads te laden.
17.18 Concurrency en idempotentie
Materialisatie en cacheverversing moeten idempotent zijn. Het opnieuw uitvoeren van dezelfde verversing mag niet leiden tot dubbele badges, dubbele readmodelregels of foutieve tellers.
Regels:
- materialisaties gebruiken stabiele natuurlijke sleutels of unieke technische sleutels;
- upserts of vervangende rebuilds zijn expliciet ontworpen;
- eventverwerking houdt rekening met dubbele levering;
- retrybare jobs registreren pogingnummer en correlation;
- bij conflicten wint brondata;
- readmodels mogen geen mutaties uitvoeren op brondata.
Voorbeeld:
Communication verwerkt MessageCreated opnieuw
- SystemMessage bestaat al
- participant-readmodel wordt opnieuw berekend
- badgewaarde wordt overschreven, niet opgehoogd bovenop oude waarde
17.19 Logging en correlation
Readmodelverversing, materialisatie en cacheherstel gebruiken correlation-id’s volgens hoofdstuk 19.
Minimale logging bij falende materialisatie:
| Veld | Doel |
|---|---|
CorrelationId | Herleidbaarheid over request/job/workflow heen. |
ModuleName | Eigenaarmodule van readmodel. |
ReadModelName | Naam van readmodel of teller. |
Scope | Niet-gevoelige scopeaanduiding, bijvoorbeeld actor/type/object-id waar toegestaan. |
SourceEvent | Mutatie of job die verversing veroorzaakte. |
AttemptNumber | Poging bij retrybare verwerking. |
ErrorCode | Classificatie van fout. |
ElapsedMs | Performance-analyse. |
Gevoelige inhoud, antwoorden, berichtteksten, volledige ticketbeschrijvingen en tokens worden niet in technische logs opgenomen.
17.20 Securitygrenzen
Readmodels kunnen gevoelige afgeleide informatie bevatten. Daarom gelden securityregels:
- readmodel-endpoints of query-services vereisen server-side autorisatie;
- cross-user badges worden nooit publiek uitleesbaar;
- beheerreadmodels zijn alleen beschikbaar binnen actieve beheercontext;
- route-id’s in readmodelqueries worden altijd gecontroleerd;
- readmodels mogen geen objectbestaan lekken bij ontbrekende toegang;
- technische identifiers worden niet onnodig in UI-viewmodels opgenomen;
- browsercaches bevatten geen autorisatie- of persoonsgevoelige readmodeldata, tenzij expliciet veilig gemaakt.
17.21 Web-compositie
OefenHub.Web bouwt pagina’s op uit publieke module-readers. Web-compositie mag meerdere modules combineren, maar blijft UI-laag.
Voorbeeld: gecombineerde docent/ouder-frontpage:
OefenHub.Web PageComposition
- vraagt TeacherCatalogSummary op bij Catalog
- vraagt TeacherStudentSummary op bij Practice of Relationships via contract
- vraagt GuardianChildSummary op bij Relationships/Practice
- vraagt TicketActionIndicator op bij Support
- composeert één ViewModel
Regels:
- Web bevat geen eigen brondata;
- Web materialiseert geen domeinreadmodels;
- Web mag UI-viewmodels tijdelijk in memory opbouwen;
- Web mag geen
DbContextgebruiken; - Web mag geen module-interne entities gebruiken;
- Web voert geen autorisatiebeslissing uit op basis van alleen zichtbare acties.
17.22 Scheduling en periodieke verwerking
OefenHub.Scheduling kan readmodeltaken plannen, starten, retryen en monitoren. De eigenaarmodule blijft verantwoordelijk voor de inhoudelijke berekening.
Voorbeelden:
| Job | Eigenaar berekening | Schedulingtaak |
|---|---|---|
| Mailboxreadmodel herstellen | Communication | Periodiek of op foutstatus opnieuw verwerken. |
| Ticketactie-indicatoren herstellen | Support | Failed/pending indicatoren opnieuw berekenen. |
| Tijdelijke exportbestanden opruimen | Reporting | Cleanup triggeren en bewaken. |
| Testruns opruimen | Practice | Opruimjob plannen en status loggen. |
| Zware beheeraggregaties verversen | Admin of eigenaarmodule | Periodieke verversing uitvoeren. |
Scheduling bewaakt jobstatus en retries, maar voert domeinlogica alleen via publieke contracten uit.
17.23 Implementatiepatronen
17.23.1 Direct query readmodel
internal sealed class ExerciseRunHistoryReader : IExerciseRunHistoryReader
{
private readonly PracticeDbContext dbContext;
private readonly IAuthorizationContext authorization;
public async Task<ExerciseRunHistoryReadModel> GetAsync(
ExerciseRunHistoryQuery query,
CancellationToken cancellationToken)
{
var scope = await authorization.RequirePracticeHistoryScopeAsync(query, cancellationToken);
return await dbContext.ExerciseRuns
.Where(run => run.StudentUserId == scope.StudentUserId)
.Where(run => run.IsCompleted && !run.IsTestRun)
.OrderByDescending(run => run.CompletedAtUtc)
.Select(run => new ExerciseRunHistoryItemReadModel(...))
.ToReadModelAsync(cancellationToken);
}
}
De reader blijft binnen Practice. De entity blijft internal. De publieke caller ziet alleen het contract en het readmodel.
17.23.2 Materialisatie na bronmutatie
Command: privébericht versturen
1. Communication valideert deelnemers en relatiecontext.
2. Communication slaat bericht en threadmutatie op.
3. Communication werkt participant-readstate en mailboxprojectie bij.
4. Communication publiceert eventueel badge-update via SignalR.
5. Web toont badge alleen wanneer de context dit toestaat.
17.23.3 Retrybare hersteljob
Job: RebuildCommunicationBadges
1. Scheduling start job met CorrelationId.
2. Communication ontvangt rebuildopdracht via contract.
3. Communication herberekent badges uit brondata.
4. Communication overschrijft materialisatiewaarden idempotent.
5. Scheduling registreert succes of foutstatus.
17.24 Teststrategie
Readmodels en tellers krijgen eigen testdekking.
| Testtype | Doel |
|---|---|
| Unit tests | Tellerdefinities, filterlogica en fallbackregels. |
| Module integration tests | EF-query’s, indexes, materialisatie en autorisatiescope binnen module. |
| Contract tests | Publieke query-services leveren alleen afgesproken readmodels. |
| Cross-module integration tests | Web-compositie en workflowgedreven invalidatie via publieke contracten. |
| Architecture tests | Geen directe DbContext/entity-toegang buiten eigenaarmodule. |
| Security tests | Geen IDOR, geen datalek bij readmodels en badges. |
| Job tests | Rebuilds, retries, idempotentie en failed-statussen. |
| Performance tests | Zware teller- en dashboardquery’s binnen afgesproken grenzen. |
Minimale testgevallen:
- lege toestand is geen autorisatiefout;
- autorisatiefout toont geen gedeeltelijke data;
- stale readmodel wordt niet als betrouwbare nulwaarde getoond;
- retry van dezelfde materialisatie is idempotent;
- badge-onderdrukking tijdens actieve oefening wijzigt geen readstate;
- ouder-/voogdreadmodel vervalt direct bij beëindigde relatie;
- docentreadmodel toont geen resultaten buiten eigen docentcontext;
- accountanonimisering verwijdert of vervangt persoonsgegevens in readmodels.
17.25 Implementatiechecklist
Bij het toevoegen of wijzigen van een readmodel, teller, badge of materialisatie moet minimaal worden gecontroleerd:
- is de eigenaarmodule duidelijk;
- staat het model onder
Models/ReadModelsof is het een Web-viewmodel; - is de brondata expliciet benoemd;
- is de tellerdefinitie eenduidig;
- is de autorisatiescope server-side vastgelegd;
- is bepaald of querymatig of gematerialiseerd wordt gewerkt;
- zijn indexen of performance-eisen beoordeeld;
- is invalidatie of rebuild beschreven;
- is fallbackgedrag vastgelegd;
- is logging/correlation geregeld;
- zijn persoonsgegevens en anonimisering beoordeeld;
- is badgegedrag tijdens actieve oefenruns beoordeeld;
- is idempotentie voor retries of rebuilds getest;
- zijn relevante Software Requirements Specification/acceptatiecriteria- en schermverwijzingen bijgewerkt wanneer de functionele betekenis wijzigt.
17.26 Implementatieverificaties
| Punt | Toelichting | Beslismoment |
|---|---|---|
| Concrete materialisatiedrempels | Per teller of overzicht meetbaar bepalen vanaf welke datasetgrootte, frequentie of responstijd materialisatie nodig wordt. | Per implementatiestory. |
| Cacheprovider | Bepalen of alleen in-memory cache volstaat of een distributed cache nodig is. | Bij performance- of schaalbesluit. |
| TickerQ-readmodeljobs | Vaststellen welke readmodelhersteljobs via TickerQ worden ingericht. | Bij uitwerking Scheduling. |
| Badge-actualiteit | Per badge bepalen of near-realtime of paginalaad voldoende is. | Bij Web-implementatie. |
| Readmodeltabellen | Voor de vastgelegde materialisatiekandidaten de concrete tabel-, view- of querytypenamen, indexes en migrations uitwerken. | Bij databaseontwerp per module. |
| Rebuildbeheer | Bepalen of beheer UI nodig is voor handmatige rebuilds. | Bij beheer/operatie-uitwerking. |
| Performancecriteria | Concrete meetwaarden per zwaar dashboard of overzicht bepalen. | Bij performancehoofdstuk en implementatie. |
| Anonimiseringsimpact | Per readmodel met persoonsgegevens vastleggen hoe anonimisering doorwerkt. | Bij privacyhoofdstuk en module-uitwerking. |