Intro, scope en uitgangspunten
1.1 Doel van het Technisch Ontwerp
Het Technisch Ontwerp beschrijft hoe OefenHub technisch wordt gerealiseerd op basis van de vastgestelde functionele en requirementbronnen. Het Technisch Ontwerp vertaalt functionele keuzes, datamodelkeuzes en architectuurkaders naar concrete implementatieafspraken voor de .NET/Blazor-applicatie, database, module-opbouw, achtergrondverwerking, realtime communicatie, PDF-export, logging, security, beheerbaarheid en teststrategie.
Het Technisch Ontwerp is geen tweede Functioneel Ontwerp en geen tweede Software Requirements Specification. Functioneel gedrag, rollen, autorisatiegrenzen, acceptatiecriteria en traceability blijven geborgd in Functioneel Ontwerp en Software Requirements Specification. Het Technisch Ontwerp beschrijft welke technische structuur, componenten, contracten, services, datatoegang, transacties, jobs en infrastructuur nodig zijn om die functionele afspraken betrouwbaar te realiseren.
1.2 Scope van het Technisch Ontwerp
De scope van het Technisch Ontwerp is de volledige OefenHub-applicatie. De technische uitwerking omvat minimaal:
| Onderwerp | Technische uitwerking |
|---|---|
| Applicatiearchitectuur | Solution-opbouw, projectstructuur, modulegrenzen, dependency-richting en deploymentvorm. |
| Frontend | Blazor/Razor-structuur, routing, layouts, componentopbouw, state, formulieren, viewmodels en page composition. |
| Identity en autorisatie | Keycloak-/identity-providerintegratie, interne accountcontext, rolcontext, server-side autorisatie en policies. |
| Domeinmodules | Technische modulegrenzen, publieke contracts, interne services, entities, events en query-ingangen. |
| Database | DbContext per module, schema-eigenaarschap, migrations, seeddata, constraints, soft links, snapshots en delete behavior. |
| Oefenmodules | Modulecontract, module-discovery, configuratie-DTO's, validatie, vraaggeneratie, antwoordcontrole, rendering en PDF-representatie. |
| Oefenruns en resultaten | Voortgangsopslag, runpayloads, statistieken, historische context, PDF-brondata en resultaatuitlezing. |
| Communicatie | Systeemberichten, privéberichten, notificaties, badges, SignalR-updates en onderdrukking tijdens oefenruns. |
| Meldingen/tickets | Technische lifecycle, beheerdersafhandeling, doorzetten naar docent, heropenen, afsluiten en logging. |
| Realtime meekijken | SignalR als transport, server-side voortgang als bron van waarheid, reconnectgedrag en LiveViewAudit. |
| PDF-export | QuestPDF, tijdelijke exportbestanden, bestandsnaamlogica, rendering, cleanup en historische reproduceerbaarheid. |
| Background jobs | TickerQ, scheduling, retrybeleid, periodieke verwerking, jobstatussen, joblogging en beheerbaarheid. |
| Logging en security | Correlation, technische foutafhandeling, access-denied logging, verdachte toegangspogingen, security headers, HSTS, CSP, secrets en omgevingconfiguratie. |
| Beheer en operatie | Monitoring, backup/restore, deployment, rollback, interne beheerinterfaces en operationele controles. |
| Teststrategie | Moduletests, integratietests, architectuurtests, acceptatieherleidbaarheid en kwaliteitsgrenzen. |
| Privacy en retentie | Accountanonimisering, persoonsgegevens in snapshots/logs/exports, retentie, tijdelijke bestanden en gegevensbescherming. |
1.3 Bronnen en volgorde van waarheid
Het Technisch Ontwerp werkt vanuit bestaande documentatiebronnen. Wanneer bronnen elkaar raken, geldt de volgende verantwoordelijkheid per documenttype.
| Bron | Rol |
|---|---|
| Software Requirements Specification | Canonieke bron voor requirements, prioriteiten, acceptatiecriteria en traceability. |
| Functioneel Ontwerp | Canonieke bron voor functioneel gedrag, rollen, domeinregels, lifecycle, autorisatiegrenzen en bron-van-waarheid op functioneel niveau. |
| Database-informatie | Primaire bron voor tabellen, kolommen, relaties, enumwaarden, constraints, snapshots, logische verwijzingen en datamodelafspraken. |
| ERD | Ondersteunende visualisatie- en navigatielaag voor relatiebegrip; niet de detailbron voor tabeldefinities. |
| Architectuur | Baseline voor C4-context, hoofdcomponenten en architectuurkaders. |
| Oefenmodule-documentatie | Bron voor moduleplatform, modulecontracten en concrete module-eisen. |
| Usecases en schermdocumentatie | Bron voor scenario's, schermgedrag, labels, lokale UI-context en interactiepatronen. |
Bij conflicten geldt:
- Functionele eisen of acceptatiecriteria worden niet in het Technisch Ontwerp aangepast, maar teruggelegd naar Functioneel Ontwerp en Software Requirements Specification.
- Datamodelwijzigingen worden niet alleen in het Technisch Ontwerp beschreven, maar teruggelegd naar database-informatie en waar nodig ERD.
- Technische keuzes die functioneel gedrag beïnvloeden, worden pas definitief wanneer ook Functioneel Ontwerp en Software Requirements Specification-impact is beoordeeld.
- Het Technisch Ontwerp mag technische vertaalkeuzes toevoegen, maar dupliceert geen volledige tabeldefinities of relationele brondetails uit database-informatie.
1.4 Architectuuruitgangspunt
OefenHub wordt technisch uitgewerkt als een middel-zware modulaire monoliet:
| Keuze | Betekenis voor OefenHub |
|---|---|
| Eén deploybare applicatie | OefenHub wordt als één applicatie gebouwd en uitgerold. Er wordt in de eerste baseline geen microservice-architectuur toegepast. |
| Eén database | De applicatie gebruikt één primaire relationele database. Er worden geen aparte databases of databaseservers per module geïntroduceerd. |
| Eén connectionstring | De applicatie gebruikt in de eerste baseline één applicatieconnectionstring voor de primaire database. |
| Meerdere moduleprojecten | Domeinen worden als afzonderlijke projecten georganiseerd om eigenaarschap en afhankelijkheden zichtbaar te maken. |
| DbContext per module | Een moduleproject heeft maximaal één eigen DbContext en daarmee maximaal één eigen databaseschema. |
| Schema per module | Database-schema's volgen module-eigenaarschap en worden in kleine letters geschreven. |
| Contracts als publieke ingang | Modules spreken elkaar niet aan via interne classes, entities of DbContexts, maar via publieke contracts, services, query-services of expliciete events. |
| Concrete oefenmodules als projecten | Technische oefenmodules krijgen eigen projecten volgens de vorm OefenHub.Modules.<ModuleCategory>.<ModuleName>. |
Deze keuze combineert de eenvoud van één applicatie en één database met strengere interne modulegrenzen. Het doel is om de applicatie beheersbaar te houden zonder de operationele complexiteit van microservices te introduceren.
De globale solution- en projectopbouw wordt uitgewerkt in Architectuuroverzicht en solution-opbouw. De dependency-richting en mapstructuur worden uitgewerkt in Applicatielagen, projectstructuur en dependency-richting.
1.4 Modulegrenzen als ontwerpprincipe
Een domeinmodule is eigenaar van haar eigen datamodel, services, interne regels, mappings, migrations en publieke module-ingangen. Andere modules mogen niet rechtstreeks afhankelijk worden van interne implementatieclasses, EF Core entities, DbContext-types of repositories van die module.
De basisregels zijn:
| Regel | Toelichting |
|---|---|
Interne implementatie is internal | Services, handlers, helpers, entities en DbContext-implementaties zijn standaard intern, tenzij zij bewust onderdeel zijn van het publieke modulecontract. |
Alleen Contracts is publiek | Interfaces, contract-DTO's en contract-enums die door andere modules gebruikt mogen worden staan onder Contracts. |
| Geen directe cross-module data access | Een module leest of muteert geen tabellen/entities van een andere module via diens DbContext. |
| Query-services voor lezen | Cross-module leesbehoeften verlopen via publieke query-services of readers. |
| Command-services voor mutaties | Cross-module mutaties verlopen via publieke command-/application-services van de eigenaar-module. |
| Events voor ontkoppelde vervolgacties | Events of scheduled jobs worden gebruikt wanneer directe synchroon-koppeling niet nodig of niet wenselijk is. |
| Web bevat geen domeinlogica | OefenHub.Web stelt UI samen, maar bevat geen businesslogica en geen directe DbContext-toegang. |
Voorbeeld van gewenste modulecommunicatie:
await relationshipInvitationService.InviteGuardianAsync(command, cancellationToken);
Voorbeeld van ongewenste modulecommunicatie:
// Niet toegestaan buiten de Relationships-module.
relationshipsDbContext.RelationshipInvitations.Add(invitation);
1.4 Datatoegang, schema's en verwijzingen
De database-informatie blijft de primaire bron voor het datamodel. Het Technisch Ontwerp beschrijft hoe dit datamodel technisch wordt gerealiseerd met EF Core, module-DbContexts, migrations, indexes, constraints en delete behavior.
De uitgangspunten voor verwijzingen zijn:
| Situatie | Standaard technische keuze |
|---|---|
| Relatie binnen hetzelfde domein/schema | Harde FK toegestaan en meestal gewenst. |
| Relatie binnen hetzelfde domein/schema met historische context | FK + snapshot waar historische leesbaarheid nodig is. |
| Relatie over module-/schemagrens | Soft link als standaard. |
| Relatie over module-/schemagrens met historische context | Soft link + snapshot als standaard. |
| Cross-module harde FK | Alleen als expliciete, gemotiveerde uitzondering. |
Voorbeeld binnen een module:
support.Tickets → support.TicketClosures
Deze relatie kan hard met FK's worden afgedwongen, omdat beide tabellen binnen dezelfde module en hetzelfde schema vallen.
Voorbeeld over modulegrens:
practice.ExerciseRuns.UserId → identity.Users.Id
Deze verwijzing bevat functioneel een gebruikers-id, maar wordt standaard als soft link met relevante snapshots behandeld. Hierdoor blijft historische oefenruninformatie leesbaar bij accountdeactivatie, anonimisering of identity-lifecyclewijzigingen.
De detailuitwerking staat in Databaseontwerp, migraties, seeddata en constraints.
1.4 Transacties, workflows en naverwerking
Cross-module workflows worden niet generiek als altijd atomair of altijd retrybaar behandeld. Per workflow wordt expliciet bepaald welke stappen onderdeel zijn van de functionele transactie en welke stappen als naverwerking mogen plaatsvinden.
Een stap hoort in de functionele transactie wanneer falen van die stap leidt tot een ongeldige, onbereikbare, misleidende of niet-herstelbare toestand. Een systeembericht kan dus kritiek zijn wanneer het de primaire ingang is voor een gebruiker, bijvoorbeeld bij een relatie-uitnodiging. Een systeembericht kan ook niet-kritiek zijn wanneer het alleen een extra notificatie is naast een al raadpleegbare bron.
| Type stap | Voorbeeld | Beleid |
|---|---|---|
| Kritieke bronmutatie | Uitnodiging accepteren en relatie aanmaken. | Atomair uitvoeren. |
| Kritieke gebruikersingang | Systeembericht dat de enige ingang is om een uitnodiging te verwerken. | Atomair met de bronactie of expliciet als verplicht onderdeel van dezelfde workflow. |
| Niet-kritieke notificatie | Extra melding dat een al zichtbare actie is uitgevoerd. | Retrybaar of beheerbaar na commit. |
| Afgeleide projectie | Dashboardteller, badge of readmodel. | Herbouwbaar of retrybaar. |
| Tijdelijke output | Tijdelijke PDF of exportbestand. | Niet leidend voor brontransactie, tenzij functioneel anders bepaald. |
Retrybare verwerking mag niet onbeperkt en onzichtbaar blijven doorgaan. Iedere retrybare job of naverwerking heeft minimaal begrensde pogingen, statusregistratie, foutregistratie, idempotentie en beheerbare foutafhandeling nodig.
De detailuitwerking van TickerQ, jobs, retries, jobstatussen en scheduling staat in Background jobs, TickerQ en periodieke verwerking.
1.4 Security en logging zonder apart securityproject
Er wordt in de eerste technische baseline geen apart OefenHub.Security project opgenomen. Dit betekent niet dat security of securitylogging buiten scope valt. Securitygerelateerde inrichting wordt expliciet uitgewerkt op de plek waar zij technisch thuishoort.
| Onderwerp | Verwachte Technische locatie |
|---|---|
| Rate limiting | Web-pipeline en securityhoofdstuk. |
| HSTS | Web-pipeline en infrastructuur/securityconfiguratie. |
| CSP | Security headers en browserbeveiliging. |
| Security headers | Middleware of extension methods in Web/Infrastructure. |
| Secrets | Environment variables, secretsbeheer en configuratiebinding. |
| Appsettings-binding | Configuratie-DTO's en environment-specifieke instellingen. |
| Verdachte toegangspogingen | Pipeline, autorisatielaag en technische logging. |
| Access-denied logging | Autorisatie- en logginghoofdstukken. |
| CorrelationId | Logging, jobs en cross-module herleidbaarheid. |
| Securitylogging | Technische foutafhandeling en operationele loggingafspraken. |
Als securityfunctionaliteit in een toekomstige wijziging voldoende groot of zelfstandig wordt, kan een apart project via een nieuw technisch ontwerpbesluit worden overwogen. Voor de eerste baseline worden securityconfiguratie en securitylogging verdeeld over Web, Infrastructure, Identity, Authorization, Scheduling en de relevante domeinmodules.
De detailuitwerking staat in Logging, audit, securitylogging en technische foutafhandeling en Security, infrastructuur, secrets en omgevingen.
1.4 Scheduling als eigen technische lifecycle
OefenHub.Scheduling is eigenaar van de technische lifecycle van jobs nadat een job succesvol is aangemaakt. Scheduling is daarmee verantwoordelijk voor plannen, persistent opslaan, triggeren, pogingentelling, retrybeleid, technische joblogging, foutstatussen en beheerbaarheid.
De domeinmodule blijft eigenaar van de businessactie die door de job wordt uitgevoerd. Een job roept domeinlogica daarom alleen aan via publieke contracten van de betreffende module.
| Verantwoordelijkheid | Eigenaar |
|---|---|
| Job aanvragen | Initiërende domeinmodule via scheduling-contract. |
| Job persistent opslaan | Scheduling/TickerQ. |
| Job triggeren | Scheduling/TickerQ. |
| Retryconfiguratie toepassen | Scheduling. |
| Domeinactie uitvoeren | Betreffende domeinmodule via publiek contract. |
| Businessregels | Betreffende domeinmodule. |
| Technische jobhistorie | Scheduling. |
| Domeinhistorie | Betreffende domeinmodule. |
TickerQ gebruikt persistente opslag zodat herstart van de applicatie geen jobresultaten of pending jobs verliest. Waar technisch ondersteund worden TickerQ-tabellen in het schema scheduling ondergebracht. De TickerQ-webinterface wordt alleen intern beschikbaar gemaakt.
1.4 Readmodels en UI-compositie
Readmodels worden primair geplaatst bij de module die eigenaar is van de data of de functionele uitlezing. Module-eigen readmodels staan onder Models/ReadModels binnen het betreffende project.
Voor samengestelde schermen, zoals frontpages of dashboards, mag OefenHub.Web viewmodels samenstellen via publieke query-services van meerdere modules. Web mag daarvoor geen module-interne entities, repositories of DbContexts gebruiken.
Voorbeeld:
OefenHub.Practice/
Models/
ReadModels/
GuardianResultReadModel.cs
ExerciseRunHistoryReadModel.cs
OefenHub.Web/
ViewModels/
PageComposition/
Er komt in de basis geen apart centraal OefenHub.ReadModels project. Alleen wanneer toekomstige persistente, domeinoverstijgende projecties met een eigen lifecycle ontstaan, wordt via een technisch ontwerpbesluit beoordeeld of een apart composition- of readmodelproject nodig is.
1.4 Technische oefenmodules
Technische oefenmodules worden als eigen projecten opgenomen met naamvorm:
OefenHub.Modules.<ModuleCategory>.<ModuleName>
De technische modulecategorie is een technische ordening en hoeft niet één-op-één overeen te komen met de functionele cataloguscategorie die leerlingen en docenten zien.
Concrete modules gebruiken OefenHub.Modules.Abstractions als primaire afhankelijkheid. Gebruik van OefenHub.SharedKernel is toegestaan wanneer een type of helper aantoonbaar generiek is en duplicatie anders strijdig zou zijn met DRY. Concrete modules mogen niet afhankelijk worden van OefenHub.Catalog, OefenHub.Practice, OefenHub.Web of module-interne platformimplementaties.
De detailuitwerking staat in Oefenmodulecontract en dynamische module-integratie.
1.4 Documentatie-impact bij structurele wijzigingen
Wanneer OefenHub via user stories wordt doorontwikkeld, moet per story expliciet worden beoordeeld of bovenliggende documentatie geraakt wordt. De minimale check is:
| Document | Impactvraag |
|---|---|
| Functioneel Ontwerp | Wijzigt functioneel gedrag, rolgedrag, lifecycle, domeinregel of schermoverschrijdende afspraak? |
| Software Requirements Specification | Wijzigt een requirement, acceptatiecriterium, prioriteit of traceability? |
| Technisch Ontwerp | Wijzigt technische structuur, services, contracten, jobs, transacties, security, logging of frontendopbouw? |
| Database-informatie | Wijzigt tabel, kolom, enumwaarde, relatie, constraint, snapshot of datamodelbron? |
| ERD | Wijzigt relatiebegrip of visualisatie van het datamodel? |
| Schermdocumentatie/usecases | Wijzigt schermgedrag, route, interactie, flow, label, tab of lokale UI-context? |
Een story die structureel iets wijzigt aan functionele regels, technische keuzes, datamodel, autorisatiegrenzen, readmodels, jobs, privacygedrag of modulecontracten is pas documentair afgerond wanneer de relevante bovenliggende documentatie is bijgewerkt of bewust als niet geraakt is beoordeeld.
1.4 Opsplitsingsregel voor hoofdstukken van het Technisch Ontwerp
hoofdstukken van het Technisch Ontwerp moeten volledig genoeg zijn om interpretatieverschillen te voorkomen, maar mogen niet zo groot worden dat zij onleesbaar of slecht onderhoudbaar worden. Wanneer een hoofdstuk meerdere zelfstandige technische onderwerpen bevat, wordt het hoofdstuk opgesplitst in subdocumenten of registers.
Een hoofdstuk komt in aanmerking voor opsplitsing wanneer:
- één onderwerp eigen ontwerpregels, tabellen, voorbeelden en validatieafspraken nodig heeft;
- een hoofdstuk structureel meerdere domeinen of technische lagen tegelijk behandelt;
- een tabel of matrix zo groot wordt dat hergebruik of onderhoud als apart register logischer is;
- besluiten en aandachtspunten anders in lopende tekst verborgen raken;
- de tekst niet meer goed als één reviewbaar hoofdstuk te beoordelen is.
Opsplitsen mag geen nieuwe bronhiërarchie introduceren. Subdocumenten blijven onderdeel van het Technisch Ontwerp en verwijzen naar het hoofdhoofdstuk of register waarin zij thuishoren.
1.4 Afbakening
Het Technisch Ontwerp beschrijft technische realisatie en ontwerpkeuzes. Het Technisch Ontwerp introduceert geen nieuwe functionele requirements buiten Software Requirements Specification of Functioneel Ontwerp. Wanneer tijdens uitwerking van het Technisch Ontwerp blijkt dat een functionele regel ontbreekt, onduidelijk is of technisch niet uitvoerbaar blijkt, wordt dit als impact teruggelegd naar Functioneel Ontwerp en Software Requirements Specification en niet stilzwijgend als technische aanname opgelost.
Het Technisch Ontwerp bevat ook geen volledige duplicatie van database-informatie. Tabellen, kolommen, enumwaarden, relationele broninformatie en constraints blijven in database-informatie. Het Technisch Ontwerp beschrijft de technische vertaling naar EF Core, migrations, schema's, DbContexts, projectstructuur, transacties, services, jobs en runtimegedrag.