Databaseontwerp, migraties, seeddata en constraints
7.1 Doel van dit hoofdstuk
Dit hoofdstuk beschrijft hoe de database-informatie van OefenHub technisch wordt vertaald naar EF Core, databaseschema's, migrations, constraints, indexes, seeddata, soft links, snapshots en technische afdwinging. De database-informatie blijft de bron voor tabellen, kolommen, relaties, enumwaarden en datamodelbetekenis. Dit Technisch Ontwerp-hoofdstuk legt vast hoe die bron in de applicatie wordt gerealiseerd en welke technische regels daarbij gelden.
Dit hoofdstuk dupliceert bewust niet alle tabeldefinities uit de database-informatie. Volledige kolomlijsten, entiteitsbeschrijvingen en relationele broninformatie blijven in de database-informatie staan. Het Technisch Ontwerp beschrijft de vertaallaag en de technische keuzes die nodig zijn om het model consistent, testbaar, migreerbaar en onderhoudbaar te implementeren.
7.2 Bronnen en afbakening
Belangrijke inputbronnen zijn:
- Database-informatie: introductie en bronafbakening
- Database-informatie: ERD-overzicht
- Software Requirements Specification: requirement-index
- Technisch Ontwerp: intro, scope en uitgangspunten
- Technisch Ontwerp: architectuuroverzicht en solution-opbouw
- Technisch Ontwerp: applicatielagen, projectstructuur en dependency-richting
Afbakening:
- De database-informatie blijft leidend voor het logische datamodel.
- Dit hoofdstuk beschrijft technische vertaalregels, geen nieuwe functionele requirements.
- Nieuwe of gewijzigde functionele regels moeten terug naar Functioneel Ontwerp en Software Requirements Specification voordat zij als databaseregel worden geïmplementeerd.
- ERD-diagrammen zijn ondersteunend voor inzicht en navigatie, maar niet de detailbron voor kolommen of constraints.
- Concrete SQL-DDL mag als afgeleide implementatie worden gegenereerd, maar wordt niet als primaire documentatiebron in dit hoofdstuk onderhouden.
7.3 Databaseprincipes
OefenHub gebruikt één relationele database binnen een middel-zware modulaire monoliet. De database is gedeeld op infrastructuurniveau, maar functioneel verdeeld in schema's die corresponderen met module-eigenaarschap. Iedere domeinmodule heeft maximaal één eigen DbContext en één eigen databaseschema. Daardoor blijft vanuit database, code en documentatie herleidbaar welk project eigenaar is van welke tabellen.
| Principe | Regel |
|---|---|
| Eén database | Alle modules gebruiken dezelfde primaire relationele database. |
| Eén connectionstring | De applicatie gebruikt in de eerste baseline één applicatieconnectionstring. |
| Schema per module | Een module met persistente data heeft één eigen databaseschema. |
| DbContext per module | Een module met persistente data heeft maximaal één eigen DbContext. |
| Geen generieke auditmodule | Audit- en historytabellen blijven bij het domein dat de bronmutatie bezit. |
| Geen generiek securityschema | Securityconfiguratie en logging worden verdeeld over Web, Infrastructure, Identity en Authorization, tenzij via een expliciet Technisch Ontwerp-besluit een zelfstandige securitymodule wordt toegevoegd. |
| Geen centrale readmodelmodule | Module-eigen readmodels blijven bij de module. Samengestelde UI-viewmodels worden via publieke query-services opgebouwd. |
| Database-informatie is bron | Het Technisch Ontwerp beschrijft vertaling en technische keuzes, niet de volledige tabelbron. |
7.4 Project, DbContext en schema
De relatie tussen project, DbContext en schema is een expliciet architectuurprincipe. Wanneer een project geen eigen persistente data bezit, heeft het ook geen eigen schema nodig. Wanneer een project wel eigen persistente data bezit, is het schema eigendom van dat project.
| Project | DbContext | Schema | Opmerking |
|---|---|---|---|
OefenHub.Identity | IdentityDbContext | identity | Interne accounts, profielkoppeling met externe identity provider en accountlifecycle. |
OefenHub.Authorization | AuthorizationDbContext | authorization | Applicatierollen, rolcontext, policies en autorisatiegerelateerde opslag indien nodig. |
OefenHub.Relationships | RelationshipsDbContext | relationships | Relaties, uitnodigingen, relatie-events en relatieafhankelijke brondata. |
OefenHub.Catalog | CatalogDbContext | catalog | Niveaus, categorieën, oefeningen, modulekoppeling en catalogushistorie. |
OefenHub.ExerciseModuleHost | ModulesDbContext indien nodig | modules | Technische modulemetadata, modulebeschikbaarheid en modulemigratiegegevens. |
OefenHub.Practice | PracticeDbContext | practice | Oefenruns, voortgang, gedeelde oefeningen, resultaten en historische runcontext. |
OefenHub.Communication | CommunicationDbContext | communication | Interne systeemberichten, privéberichtthreads, systeemberichttemplates en communicatiehistorie. |
OefenHub.Mail | MailDbContext | mail | Applicatiegestuurde externe e-mail, mailtemplates, templatehistory en mailverzendpogingen. |
OefenHub.Support | SupportDbContext | support | Meldingen, tickets, ticketdiscussie, sluitingen, heropenverzoeken en supporthistorie. |
OefenHub.LiveMonitoring | LiveMonitoringDbContext | live | Live-meekijksessies, live-audit en live-gerelateerde status waar persistent nodig. |
OefenHub.Admin | AdminDbContext | admin | Beheerbare content, vaste pagina’s, footercontent, URL-records, popups, site-instellingen, systeemnotificaties, featuretoggles en beheerlogs. |
OefenHub.Reporting | ReportingDbContext indien nodig | reporting | Persistente rapportage- of exportmetadata wanneer dit via een expliciet Technisch Ontwerp-besluit noodzakelijk wordt. |
OefenHub.Scheduling | SchedulingDbContext | scheduling | TickerQ-persistence, jobstatus, technische jobhistorie en retrybare verwerking. |
OefenHub.Web | geen | geen | UI, routing, pipeline en page composition; geen eigen datalaag. |
OefenHub.Infrastructure | geen standaardcontext | geen standaardschema | Technische adapters en infrastructuurconfiguratie; alleen een schema bij expliciet vastgelegde noodzaak. |
OefenHub.SharedKernel | geen | geen | Gedeelde generieke types; geen persistente eigenaarschap. |
OefenHub.Modules.Abstractions | geen | geen | Contracten voor technische oefenmodules. |
OefenHub.Modules.<ModuleCategory>.<ModuleName> | geen standaardcontext | geen standaardschema | Concrete oefenmodules bevatten normaal geen eigen databaseopslag. |
Wanneer tijdens onderhoud of uitbreiding blijkt dat een project meerdere onafhankelijke schema's of DbContexts nodig heeft, is dat een signaal dat het project mogelijk te breed is geworden en moet worden opgesplitst of herontworpen.
7.4.1 Configuratie- en contentbeheer binnen het adminschema
Het adminschema bevat beheerbare configuratie- en contentrecords die via Site Instellingen of beheerflows worden onderhouden. De database-informatie blijft leidend voor de exacte tabel- en kolomdefinities; dit Technisch Ontwerp legt de technische eigenaarschapsgrenzen vast.
| Beheergebied | Tabellen of technische bron | Technische regel |
|---|---|---|
| Popupbeheer | PopupDetails, PopupDetailsHistory, PopupDetailsHistoryItems | Nieuwe popupkeys ontstaan via code en migratie. Beheer wijzigt alleen toegestane content- en presentatievelden. |
| Featuretoggles | SiteFeatureToggles, SiteFeatureToggleHistory | Alleen expliciet togglebare functies worden opgenomen. Verplichte kernfunctionaliteit wordt niet als featuretoggle gemodelleerd. |
| Systeeminstellingen | SystemSettings | Nieuwe keys ontstaan via code en migratie. De beheerinterface wijzigt alleen bestaande keys binnen datatype- en validatiegrenzen. |
| Contentblokken | ContentBlocks, ContentBlockHistory | Tekstuele content is beheerbaar; layout, blokpositie en renderstructuur blijven codegedreven. |
| URL-records | SiteLinks, SiteLinkHistory | Interne en externe URL’s worden server-side gevalideerd. In gebruik zijnde links worden niet hard verwijderd. |
| Footerindeling | FooterSections, FooterLinkAssignments, FooterLinkAssignmentHistory | Footerplaatsingen zijn contextgebonden en volgen de vaste footerstructuur. |
| Systeemnotificaties | SiteNotifications, SiteNotificationHistory | Frontpage-overlays zijn gescheiden van mailbox-systeemberichten en popupregister-popups. |
| Beheerlogging | ManagementLogEntries of domeinspecifieke history | Beheeracties worden vastgelegd bij het eigenaar-domein of in admin wanneer de bronmutatie bij beheerbare sitecontent hoort. |
Systeemberichttemplates kunnen via de beheerinterface worden aangepast, maar de technische bron en verwerking horen bij het communicatiedomein wanneer de templates door Communication worden gebruikt. De beheerroute mag deze templatebron niet dupliceren in het adminschema.
7.5 Naamgevingsconventies
Naamgeving moet de modulegrenzen leesbaar maken zonder kunstmatige afkortingen te introduceren. Schema's zijn bewust kort, Engelstalig en in kleine letters. Tabellen en kolommen gebruiken PascalCase.
| Object | Conventie | Voorbeeld |
|---|---|---|
| Database schema | kleine letters, Engels, betekenisvol | practice, communication, support |
| Tabel | PascalCase, meervoud of bestaande database-informatie-conventie | ExerciseRuns, SystemMessages, TicketClosures |
| Kolom | PascalCase | CreatedAtUtc, UserId, CategoryNameSnapshot |
| Primary key | Id, tenzij database-informatie bewust anders bepaalt | Id |
| Foreign-keyachtige waarde | <Object>Id | UserId, ExerciseRunId |
| Snapshotveld | <Naam>Snapshot of duidelijke contextnaam | UserDisplayNameSnapshot |
| DbContext | <ModuleName>DbContext | PracticeDbContext |
| Entity configuration | <EntityName>Configuration | ExerciseRunConfiguration |
| Migration | timestamp plus korte Engelstalige beschrijving | 20260601090000_AddExerciseRunSnapshots |
| Index | IX_<Table>_<Columns> | IX_ExerciseRuns_UserId_CompletedAtUtc |
| Unique constraint | UX_<Table>_<Columns> | UX_RelationshipInvitations_NormalizedEmail_Type_Status |
Voorbeelden:
practice.ExerciseRuns
practice.ExerciseRunProgress
communication.SystemMessages
support.TicketClosures
relationships.UserRelationships
7.6 EF Core-projectstructuur per module
Iedere persistente module gebruikt dezelfde herkenbare basisstructuur. Niet iedere map hoeft altijd aanwezig te zijn, maar wanneer het type artefact bestaat, hoort het in de bijbehorende map.
OefenHub.Practice/
Contracts/
Models/
Enums/
Data/
PracticeDbContext.cs
Entities/
Configurations/
Migrations/
Models/
Commands/
Queries/
ReadModels/
Enums/
Services/
Interfaces/
Events/
Helpers/
Extensions/
| Map | Betekenis |
|---|---|
Contracts | Publieke module-ingang voor andere modules. |
Contracts/Models | DTO's die onderdeel zijn van publieke modulecontracten. |
Data | Module-eigen DbContext en databasevertaling. |
Data/Entities | EF Core entities; standaard internal. |
Data/Configurations | IEntityTypeConfiguration<T>-configuraties. |
Data/Migrations | EF Core migrations voor de module-DbContext. |
Models | Module-interne DTO's en modellen. |
Models/ReadModels | Module-eigen readmodels voor queryresultaten. |
Services | Implementatie van module-usecases, command-services en query-services. |
Services/Interfaces | Interne service-interfaces, niet bedoeld als publieke modulecontracten. |
Events | Module-interne of expliciet gepubliceerde application events. |
Helpers | Kleine module-lokale helpers zonder domeinbeslissingen. |
Extensions | Registratie- en hulpextensies, zoals dependency-injectionregistratie. |
7.7 DbContext-regels
Een module-DbContext beheert alleen de tabellen van het eigen schema. De DbContext bevat geen DbSets voor entities van andere modules. Andere modules krijgen geen directe toegang tot de DbContext of entities, maar gebruiken publieke contracten, command-services en query-services.
| Regel | Toelichting |
|---|---|
| Eén schema per DbContext | modelBuilder.HasDefaultSchema("practice") of expliciete schema-configuratie per entity. |
| Geen cross-module DbSets | PracticeDbContext bevat geen Users, SystemMessages of Tickets. |
Entities standaard internal | Andere modules mogen niet rechtstreeks entitytypen gebruiken. |
| DbContext standaard module-intern | Alleen registratie en module-services gebruiken de context. |
| Configuraties per entity | Fluent API-configuratie staat in Data/Configurations. |
| Geen businesslogica in DbContext | DbContext bevat mapping en persistenceconfiguratie, geen domeinbeslissingen. |
| Geen UI-afhankelijkheden | DbContext en entities referencen nooit OefenHub.Web. |
Voorbeeld:
internal sealed class PracticeDbContext : DbContext
{
internal DbSet<ExerciseRun> ExerciseRuns => Set<ExerciseRun>();
internal DbSet<ExerciseRunProgress> ExerciseRunProgress => Set<ExerciseRunProgress>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("practice");
modelBuilder.ApplyConfigurationsFromAssembly(typeof(PracticeDbContext).Assembly);
}
}
De exacte zichtbaarheid van DbContext en DbSet kan per technische noodzaak worden aangepast, maar directe toegang vanuit andere modules blijft niet toegestaan.
7.8 Entity- en propertyvertaalregels
Entities volgen de database-informatie. De entity is een technische vertaling van een tabel en vormt niet de publieke API van een module. Publieke communicatie verloopt via contractmodellen of readmodels.
| Onderwerp | Regel |
|---|---|
| Entitynaam | Sluit aan bij tabelnaam zonder schema. |
| Primary key | Id als technische sleutel, tenzij database-informatie anders bepaalt. |
| Nullable | Database-nullability volgt functionele betekenis uit database-informatie. |
| UTC-tijdvelden | Tijdvelden die proces- of auditmomenten vastleggen gebruiken UTC. |
| Soft delete | Alleen toepassen waar database-informatie of domeinregels historische zichtbaarheid vereisen. |
| Rowversion/concurrency | Gebruiken bij records met relevante gelijktijdige wijzigingsrisico's. |
| Navigatieproperties | Binnen module toegestaan; over modulegrens standaard vermijden. |
| Owned types | Alleen gebruiken voor duidelijke value objects binnen dezelfde module. |
| JSON/base64-payloads | Alleen gebruiken waar modulespecifieke variatie relationele uitsplitsing ongeschikt maakt. |
Voorbeeld van een module-interne entity:
internal sealed class ExerciseRun
{
public Guid Id { get; private set; }
public Guid UserId { get; private set; }
public string? UserDisplayNameSnapshot { get; private set; }
public Guid ExerciseId { get; private set; }
public string ExerciseNameSnapshot { get; private set; } = string.Empty;
public DateTime CreatedAtUtc { get; private set; }
public DateTime? CompletedAtUtc { get; private set; }
}
In dit voorbeeld hoeft UserId geen harde FK naar identity.Users te hebben. De waarde is een soft link naar het accountdomein en de snapshotvelden borgen historische leesbaarheid.
7.9 Relaties: functioneel versus technisch afdwingen
Een functionele relatie betekent dat records inhoudelijk naar elkaar verwijzen. Dat betekent niet automatisch dat de database een harde foreign key moet afdwingen. Per relatie wordt expliciet gekozen hoe de relatie technisch wordt vastgelegd.
| Relatietype | Standaard technische vorm | Voorbeeld |
|---|---|---|
| Binnen dezelfde module zonder historische uitzondering | Harde FK | support.TicketClosures.TicketId naar support.Tickets.Id |
| Binnen dezelfde module met historische context | FK + snapshot | Sluiting met actuele relatie plus tekstuele statusmomentopname |
| Over modulegrens zonder historische context | Soft link | practice.ExerciseRuns.UserId verwijst logisch naar identity.Users.Id |
| Over modulegrens met historische context | Soft link + snapshot | practice.ExerciseRuns.UserId plus UserDisplayNameSnapshot |
| Polymorfe of beperkte domeinverwijzing | Applicatielogica + validatie | communication.SystemMessages.EntityType + EntityId |
| Audit/history naar actor | Soft link + actor snapshot | admin.ManagementLogEntries.ActorUserId plus naam/rolsnapshot |
| Uitzonderlijke cross-module harde afhankelijkheid | Expliciet besluit nodig | Alleen na motivatie in het Technisch Ontwerp of besluitenregister |
Basisregel:
Binnen modulegrenzen zijn harde FK's gewenst waar zij de integriteit versterken. Over modulegrenzen zijn soft links en soft link + snapshot de standaard. Cross-module harde FK's zijn uitzonderingen en moeten expliciet worden gemotiveerd.
Deze keuze voorkomt dat één module de lifecycle van een andere module onbedoeld blokkeert. Zij ondersteunt ook historische reconstructie, accountanonimisering, modulemigraties en soft-deactivatie van relaties zonder dat oude resultaten of auditrecords technisch onleesbaar worden.
7.10 Beslismatrix voor relatie-afdwinging
Gebruik onderstaande beslismatrix bij iedere relatie die in database-informatie of Technisch Ontwerp wordt uitgewerkt.
| Vraag | Als het antwoord ja is | Gevolg |
|---|---|---|
| Liggen beide tabellen in hetzelfde schema en dezelfde DbContext? | Ja | Harde FK is het uitgangspunt. |
| Moet het doelrecord historisch kunnen verdwijnen, anonimiseren of inactief worden zonder bronrecord te breken? | Ja | Gebruik snapshot en vermijd cascade delete. |
| Is de relatie over modulegrenzen heen? | Ja | Gebruik standaard soft link of soft link + snapshot. |
| Is het doelrecord de enige geldige bron voor actuele verwerking? | Ja | Overweeg expliciete validatie via modulecontract. |
Is de relatie polymorf of afhankelijk van EntityType? | Ja | Gebruik applicatielogica, allowlist en autorisatiecontrole. |
| Moet rapportage oude naamgeving tonen? | Ja | Snapshot verplicht. |
| Zou een hard FK de accountlifecycle, anonimisering of migratie blokkeren? | Ja | Geen hard FK, tenzij expliciet besluit. |
| Is een cross-module hard FK toch noodzakelijk? | Ja | Leg motivatie, eigenaar, delete behavior en migratie-impact vast. |
7.11 Soft links
Een soft link is een opgeslagen sleutelwaarde die functioneel verwijst naar een record in een andere module, zonder harde database-FK. De waarde gebruikt bij voorkeur hetzelfde sleuteltype als het doelrecord, bijvoorbeeld Guid, maar de database dwingt de relatie niet rechtstreeks af.
Voorbeeld:
practice.ExerciseRuns.UserId
Deze waarde verwijst functioneel naar een account in identity.Users, maar practice bezit geen Users-entity en krijgt geen DbSet naar identity.Users. De Practice-module kan actuele autorisatie of context laten controleren via publieke contracten van Identity of Authorization.
Soft links vereisen aanvullende discipline:
- validatie vindt plaats via publieke modulecontracten;
- referentiële integriteit wordt functioneel bewaakt door application services;
- historische leesbaarheid wordt geborgd via snapshotvelden;
- queries over soft links gebruiken publieke query-services of readmodels, niet directe cross-module joins vanuit modulecode;
- technische rapportage mag soft links tonen als diagnosewaarde, maar gebruikersschermen tonen betekenisvolle snapshot- of readmodelinformatie.
7.12 Snapshotbeleid
Snapshotvelden leggen historische context vast zoals die gold op het moment van een functioneel relevante gebeurtenis. Zij zijn bedoeld om resultaten, meldingen, audit, gedeelde oefeningen, PDF-export en historische overzichten leesbaar te houden wanneer actuele brondata later wijzigt.
| Aspect | Regel |
|---|---|
| Vulmoment | Vastleggen op het moment waarop de historische context ontstaat. |
| Immutability | Snapshotvelden worden na vastlegging niet stilzwijgend bijgewerkt. |
| Fallback | Snapshot wordt gebruikt wanneer actuele brondata ontbreekt, gewijzigd is of niet meer passend is. |
| Persoonsgegevens | Snapshotwaarden met persoonsgegevens vallen onder anonimisering en retentiebeleid. |
| PDF/export | Export gebruikt historische runcontext en snapshots waar relevant. |
| Modulemigratie | Migraties mogen historische snapshots niet herschrijven tenzij expliciet als herstelactie besloten. |
| Beheerwijzigingen | Latere wijzigingen aan naam, kleur, icoon of profielgegevens wijzigen bestaande snapshots niet automatisch. |
Voor oefenruns geldt als voorkeursregel dat contextsnapshots bij het starten van de run worden gevuld. Daarmee blijft duidelijk met welke leerling-, niveau-, categorie-, oefening- en modulecontext de run daadwerkelijk is ontstaan. Wanneer een specifieke flow beter bij afronding kan snapshotten, moet dat per flow worden gemotiveerd.
Voorbeelden van snapshotvelden:
| Domein | Mogelijke snapshots |
|---|---|
| Practice | niveau-, categorie-, oefening-, module- en gebruikersnaam op runmoment. |
| Communication | afzendernaam, rolcontext of systeemreferentie op verzendmoment. |
| Support | meldernaam, rolcontext, technische pagina- en browsersnapshot op meldmoment. |
| Admin | actorrol, oude/nieuwe waarde en reden bij beheeractie. |
| LiveMonitoring | viewerrolnaam en leerlingcontext bij start van meekijken. |
7.13 Delete behavior, soft delete en historische records
Delete behavior moet historische reconstructie en privacy waarborgen. Hard delete wordt beperkt tot technische tijdelijke data waarvoor geen functionele reconstructie nodig is. Functionele domeinrecords worden meestal gedeactiveerd, gesloten, geanonimiseerd of historisch bewaard.
| Situatie | Voorkeur |
|---|---|
| Functionele relatie beëindigen | Soft-deactivatie met datum, actor en reden indien nodig. |
| Account verwijderen | Anonimisering en beëindiging van toegang, geen generiek hard delete van historie. |
| Relatie beëindigen | IsActive = false of vergelijkbare lifecyclevelden. |
| Ticket sluiten | Sluitregistratie in TicketClosures, geen verwijderen van ticket. |
| Privéthread verwijderen uit mailbox | Participantgebonden zichtbaarheid aanpassen, thread niet hard verwijderen. |
| Tijdelijke testdata | Hard delete of cleanup toegestaan volgens jobbeleid. |
| Tijdelijke PDF/exportbestanden | Periodieke cleanup toegestaan; brondata blijft database. |
| Readmodels/cache | Herbouwbaar; mag worden verwijderd en opnieuw opgebouwd. |
Cascade delete wordt terughoudend gebruikt. Binnen één module kan cascade delete technisch passend zijn voor zuivere child records zonder zelfstandige historie. Over modulegrenzen wordt cascade delete niet gebruikt.
7.14 Enum- en sleutelsetstrategie
Enumwaarden en vaste waardelijsten worden niet ad hoc in meerdere lagen gedupliceerd. De database-informatie beschrijft de functionele waarden. Het Technisch Ontwerp legt vast hoe zij technisch worden vertaald.
| Type waardelijst | Technische aanpak |
|---|---|
| Stabiele technische enum | C# enum of gesloten value object, met expliciete databaseconversie. |
| Beheerbare referentiedata | Tabel met idempotente seeddata. |
| Functionele status met lifecycle | Tabel of gesloten enum afhankelijk van wijzigingsbehoefte. |
| UI-labels | Niet als bron in enumcode; labels via applicatielaag of beheerbare content waar passend. |
| Modulecontractwaarden | In OefenHub.Modules.Abstractions of modulecontracten vastleggen. |
Voorbeelden:
| Domein | Waardelijst | Verwachte aanpak |
|---|---|---|
| Support | ticketstatussen | gesloten set of referentietabel volgens database-informatie. |
| Support | afsluitstatussen | referentiedata met idempotente seeddata. |
| Communication | systeembericht-entitytypes | gesloten set met allowlist en applicatielogica. |
| Relationships | uitnodigingsstatussen | gesloten domeinset. |
| Catalog | categoriehistorie-actietypes | gesloten domeinset. |
Wanneer een waardelijst door beheer gewijzigd moet kunnen worden, is een C# enum meestal ongeschikt als enige bron. Wanneer waarden juist codegedreven en niet beheerbaar zijn, is een gesloten enum of value object beter.
7.15 Seeddata
Seeddata is module-eigen, idempotent en expliciet getypeerd. Seeddata mag beheerbare content niet stilzwijgend overschrijven. De module die eigenaar is van het schema is ook eigenaar van de seedstrategie voor dat schema.
| Seedtype | Voorbeeld | Gedrag |
|---|---|---|
| Initiële technische seed | standaardrolcodes, basisstatussen | Alleen aanmaken wanneer ontbrekend. |
| Referentiedata | ticketstatussen, afsluitstatussen | Idempotent synchroniseren op sleutel/code. |
| Beheerbare content en templates | popupdefinities, contentblokken, footerrecords, URL-records, systeeminstellingen, featuretoggles, systeemnotificaties en systeemberichttemplates | Initieel vullen wanneer ontbrekend; bestaande beheerwijzigingen niet stilzwijgend overschrijven. |
| Modulemetadata | geregistreerde oefenmodules | Via modulehost/catalogusproces en idempotente herkenning. |
| Development data | testgebruikers, voorbeeldruns | Alleen in development of testomgeving. |
| Testdata | integratie- en acceptatietests | Niet in productie-seed opnemen. |
Seeddata moet stabiele technische sleutels gebruiken. Naam of label alleen als sleutel gebruiken wanneer dit functioneel gegarandeerd stabiel is. Voor beheerbare content moet de technische sleutel bijvoorbeeld een PopupKey, TemplateKey, BlockKey, FeatureKey, SettingKey, NotificationKey of vergelijkbare stabiele key zijn.
Seeddata mag niet automatisch bij iedere applicatiestart alle waarden overschrijven. Bij productie wordt seeddata uitgevoerd als onderdeel van gecontroleerde migratie- of deploymentlogica.
7.15.1 Beheerbare seeddata voor popups, templates en instellingen
Voor beheerbare seeddata geldt een vaste V1.0-baseline. De seed levert alleen de initiële technische sleutelset en veilige beginwaarden. Daarna is de beheerinterface eigenaar van de beheerbare velden, binnen de validatieregels van het eigenaar-domein. Een release mag beheerwijzigingen dus niet ongemerkt terugzetten naar de oorspronkelijke seedwaarde.
| Objectgroep | Eigenaar | Stabiele sleutel | Seedgedrag | Beheer na seed |
|---|---|---|---|---|
| Popupregister | Admin | PopupKey | Aanmaken wanneer de key ontbreekt. Initieel vullen met titel, tekst, variant en technische metadata volgens database-informatie. | Beheer mag toegestane content- en presentatievelden wijzigen. Key, technische actie, renderer en structurele variant blijven codegedreven of read-only. |
| Systeemberichttemplates | Communication | TemplateKey | Aanmaken wanneer de key ontbreekt. Initieel vullen met onderwerp, body, kanaal/context en toegestane placeholderdefinitie. | Beheer mag bestaande template-inhoud wijzigen binnen placeholder- en validatiegrenzen. Key, renderer, doelgroepcontext en placeholdercontract blijven codegedreven. |
| Systeeminstellingen | Admin | SettingKey | Aanmaken wanneer de key ontbreekt. Initieel vullen met defaultwaarde, datatype, invoervorm en validatieregel volgens code/database-informatie. | Beheer mag de waarde wijzigen binnen datatype- en validatiegrenzen. Nieuwe keys, datatype, validatie en invoervorm ontstaan alleen via code en migratie. |
| Featuretoggles | Admin | FeatureKey | Aanmaken wanneer de key ontbreekt voor expliciet togglebare functionaliteit. | Beheer mag de togglewaarde en toegestane beheerbare metadata wijzigen. De technische featurekey en betekenis blijven codegedreven. |
| Systeemnotificaties | Admin | NotificationKey of vaste technische sleutel waar van toepassing | Alleen vaste of initiële notificatiedefinities seeden wanneer zij ontbreken. | Planning, inhoud en actiefstatus zijn beheerbaar binnen autorisatie- en validatiegrenzen. |
| Content- en linkblokken | Admin | BlockKey, LinkKey of vaste plaatsingskey | Initieel vullen wanneer een vaste pagina-, frontpage-, footer- of linkpositie ontbreekt. | Tekst, URL en plaatsing zijn beheerbaar binnen de toegestane velden. Layout en renderstructuur blijven codegedreven. |
Seedlogica voor deze objectgroepen volgt deze regels:
- Eerst worden schema-migrations toegepast; daarna pas seedlogica.
- De seed zoekt op stabiele technische key, niet op label of weergavetekst.
- Ontbrekende records worden aangemaakt met veilige defaults.
- Bestaande records worden niet overschreven wanneer zij beheerbaar zijn.
- Wanneer een technische definitie wijzigt, gebeurt dat via expliciete migration, backfill of beheeractie met history/audit, niet via stille reseed.
- In productie wordt beheerbare seeddata niet bij iedere applicatiestart blind uitgevoerd.
- Seeds bevatten geen secrets, persoonsgegevens, tokens of omgeving-specifieke productiewaarden.
- Dev- en testseeddata blijven gescheiden van productie-seeddata.
Als een release een bestaande key functioneel moet wijzigen, moet de migration of beheeractie expliciet aangeven of bestaande beheerwaarden behouden blijven, gemigreerd worden of na beheerreview worden aangepast. Die keuze moet in release- of migratienotities herleidbaar zijn.
7.16 Migrations
Iedere module met een DbContext beheert eigen migrations onder Data/Migrations. Migrations worden niet handmatig in een centrale map samengevoegd. De deploymentlaag moet wel weten welke modulemigrations bestaan en in welke volgorde zij veilig kunnen worden uitgevoerd.
| Regel | Toelichting |
|---|---|
| Migrations per module | Iedere DbContext heeft eigen migration history en migrationbestanden. |
| Schema-eigenaarschap | Een migration wijzigt alleen het schema van de eigen module, tenzij expliciet anders besloten. |
| Geen verborgen cross-module DDL | Een migration maakt niet stilzwijgend tabellen of constraints in andere schema's. |
| Volgorde expliciet | Deployment voert migrations in een vaste, herhaalbare volgorde uit. |
| Seed na schema | Seeddata wordt pas uitgevoerd nadat schema-migrations succesvol zijn. |
| Idempotentie | Seeds en technische datawijzigingen zijn herhaalbaar zonder dubbele records. |
| Rollbackbeleid | Rollback wordt per release beoordeeld en niet blind aan EF down-migrations overgelaten. |
| Backup voor productie | Productiemigraties vereisen voorafgaand backup-/restorebeleid zoals uitgewerkt in beheerhoofdstuk. |
Voorbeeld van deploymentvolgorde:
1. identity
2. authorization
3. relationships
4. catalog
5. modules
6. practice
7. communication
8. support
9. live
10. admin
11. reporting
12. scheduling
Deze volgorde is een operationele afspraak, geen uitnodiging om cross-module harde FK's te introduceren. Omdat cross-module harde FK's uitzonderingen zijn, moeten de meeste migrations onafhankelijk uitvoerbaar blijven.
7.17 EF Core migration history
Bij meerdere DbContexts moet worden voorkomen dat migration history onduidelijk wordt. De V1.0-baseline gebruikt daarom per persistente module-DbContext een eigen EF Core migration history in het eigen databaseschema van de module.
| Keuze | Baseline |
|---|---|
| Eén centrale history zonder contextonderscheid | Niet toegestaan. |
| Per DbContext eigen historytabelnaam in één schema | Alleen toegestaan wanneer het databaseplatform of providerbeperking history per schema onmogelijk maakt. |
| Per DbContext history in eigen schema | Verplicht als voorkeursinrichting. |
| Handmatig bijhouden buiten EF | Niet gebruiken zolang EF Core de historytabel kan beheren. |
De standaardnaam van de historytabel blijft __EFMigrationsHistory. De scheiding zit in het schema. Voorbeelden:
| DbContext | Schema | Historytabel |
|---|---|---|
IdentityDbContext | identity | identity.__EFMigrationsHistory |
AuthorizationDbContext | authorization | authorization.__EFMigrationsHistory |
PracticeDbContext | practice | practice.__EFMigrationsHistory |
CommunicationDbContext | communication | communication.__EFMigrationsHistory |
SchedulingDbContext | scheduling | scheduling.__EFMigrationsHistory |
ReportingDbContext | reporting | Alleen wanneer ReportingDbContext daadwerkelijk persistent wordt gebruikt. |
Technische inrichting:
options.UseSqlServer(
connectionString,
sql => sql.MigrationsHistoryTable("__EFMigrationsHistory", moduleSchema));
Regels:
- iedere module-DbContext configureert expliciet de eigen historytabel in het eigen schema;
- migrations blijven in de module-eigen
Data/Migrations-map; - een migration schrijft geen historyrijen in een ander moduleschema;
- de deploymentvolgorde uit hoofdstuk 7.16 blijft leidend voor het uitvoeren van migrations;
- een bestaande productiehistorytabel wordt niet stilzwijgend verplaatst of hernoemd; zo'n wijziging vereist een expliciete migratie-/releaseprocedure.
Validatie van deze keuze gebeurt met een lege-database-migrationtest en, zodra er een vorige baseline bestaat, een upgradepadtest. De test moet aantonen dat alle modulemigrations toepasbaar zijn zonder conflicterende __EFMigrationsHistory-tabellen.
7.18 Indexen en constraints
Indexen en constraints ondersteunen functionele integriteit en performance, maar mogen modulegrenzen niet onbedoeld doorbreken. Indexen worden vooral bepaald door autorisatiechecks, geschiedenisfilters, frontpage-/dashboardqueries, badges, online-overzichten, live-meekijken en beheerfilters.
| Querypatroon | Mogelijke indexrichting |
|---|---|
| Geschiedenis per gebruiker en periode | UserId, CompletedAtUtc, ExerciseId |
| Ouder-/voogdresultaten per kind | StudentUserId, CompletedAtUtc |
| Docentgeschiedenis binnen niveaucontext | LevelId, StudentUserId, CompletedAtUtc |
| Openstaande relatie-uitnodigingen | ToUserId, ToEmailNormalized, Status, ExpiresAtUtc |
| Ongelezen berichten | RecipientUserId, ReadAtUtc, SentAtUtc |
| Tickets per status | Status, UpdatedAtUtc |
| Wacht-op-mij indicator | CreatedByUserId, Status |
| Actieve live sessies | StudentUserId, ExerciseRunId, EndedAtUtc |
| Scheduler pending jobs | Status, NextAttemptAtUtc, JobType |
Unique constraints worden gebruikt waar zij echte functionele uniciteit afdwingen binnen het eigen domein. Voorbeelden:
| Domein | Mogelijke unieke constraint |
|---|---|
| Relationships | geen dubbele actieve relatie voor dezelfde combinatie van gebruikers, relatietype en rolcontext. |
| Relationships | geen conflicterende openstaande uitnodiging voor dezelfde combinatie waar dat functioneel verboden is. |
| Catalog | technische modulecode of categoriekey waar die code uniek moet zijn. |
| Communication | templatekey of popupkey binnen het eigen beheerbereik. |
| Scheduling | idempotency key voor jobaanmaak indien jobtype dit vereist. |
Constraints die cross-module integriteit suggereren, worden alleen toegevoegd na expliciet besluit.
7.19 JSON/base64-payloads
OefenHub gebruikt JSON/base64-payloads voor gegevens waarvan de structuur per technische oefenmodule verschilt. Dit geldt met name voor modulespecifieke configuratie, vraaginhoud, antwoordstructuren en voortgangsdetails.
| Payloadtype | Eigenaar | Regel |
|---|---|---|
| Oefeningconfiguratie | catalog en modulecontract | Generiek opgeslagen, modulespecifiek gevalideerd. |
| Vraag-/antwoordpayload | practice en modulecontract | Bron voor modulespecifieke reconstructie. |
| Voortgangsdetails | practice | Server-side opgeslagen na bevestigde stappen. |
| PDF-renderdata | modulecontract en practice | Historische runcontext blijft leidend. |
Payloads zijn geen vrijbrief om relationele data te vermijden. Uniforme query- en filterwaarden die nodig zijn voor geschiedenis, resultaten, live meekijken, dashboards of autorisatie worden als gewone kolommen opgeslagen. Payloads bewaren modulespecifieke inhoud die niet stabiel relationeel te modelleren is.
7.20 Readmodels, cache en materialisatie
Module-eigen readmodels staan onder Models/ReadModels van de eigenaar-module. Readmodels zijn afgeleide weergave- of querymodellen en geen tweede bron van waarheid. Fysieke materialisatie is alleen toegestaan wanneer performance, tellerconsistentie of querycomplexiteit dit rechtvaardigt.
| Readmodeltype | Plaats | Bronstatus |
|---|---|---|
| Module-eigen queryresultaat | Models/ReadModels binnen module | Afgeleid uit eigen brondata. |
| Samengesteld UI-viewmodel | OefenHub.Web/ViewModels of PageComposition | Samengesteld via publieke query-services. |
| Fysiek gematerialiseerd readmodel | Schema van eigenaar-module | Herbouwbaar of met expliciet herstelbeleid. |
| Badge/teller | Eigenaar van de tellerdefinitie | Afgeleid; geen autorisatiebron. |
| Cache | Technische laag | Mag nooit bron van autorisatie of historische waarheid zijn. |
Wanneer een readmodel data uit meerdere modules combineert, mag het niet rechtstreeks DbContexts van meerdere modules gaan koppelen. De samenstelling loopt via publieke query-services of via een expliciet gematerialiseerd model met vastgelegde eigenaar en herstelstrategie.
7.21 Scheduling-persistence en TickerQ
OefenHub.Scheduling krijgt een eigen schema scheduling wanneer TickerQ en aanvullende jobtabellen persistente opslag nodig hebben. Een applicatieherstart mag geen geplande of retrybare jobs verliezen. Daarom moet jobstatus persistent zijn.
| Onderdeel | Regel |
|---|---|
| Jobopslag | Persistent in het scheduling schema. |
| Joblifecycle | Eigendom van OefenHub.Scheduling na succesvolle jobaanmaak. |
| Jobuitvoering | Domeinactie via publiek contract van de eigenaar-module. |
| Retrybeleid | Per jobtype configureerbaar, begrensd en gelogd. |
| Foutstatus | Failed jobs blijven beheerbaar zichtbaar. |
| Correlation | CorrelationId wordt door de volledige jobketen doorgegeven. |
| TickerQ-dashboard | Alleen intern beschikbaar en niet publiek ontsloten. |
Bij implementatie moet worden gecontroleerd hoe TickerQ het schema, de persistence-tabellen en de beheerinterface configureert. Wanneer TickerQ eigen tabelnamen of schema-instellingen afdwingt, wordt dit in hoofdstuk 18 en 21 concreet vastgelegd.
7.22 Transactions en cross-module consistentie
De database ondersteunt meerdere DbContexts op dezelfde connectionstring. Een gebruikersactie of job kan meerdere modules raken, maar de transaction boundary wordt per workflow bepaald. De V1.0-baseline gebruikt geen brede generieke cross-module transactie als standaardpatroon. De functionele eigenaar voert de primaire bronmutatie uit; alleen aantoonbaar kritieke stappen worden binnen dezelfde transactionele boundary gehouden.
| Situatie | Richting |
|---|---|
| Alleen eigen modulebrondata | Module-eigen transactie. |
| Meerdere kritieke mutaties die samen één functionele uitkomst vormen | Atomaire uitvoering binnen dezelfde applicatieflow en database wanneer technisch passend; anders expliciete failed-status of compensatiebeleid. |
| Niet-kritieke naverwerking | Retrybaar via TickerQ-job, pending action, cache-invalidatie of readmodelrebuild. |
| Systeembericht als enige functionele ingang | Beschouwen als kritiek binnen die workflow. |
| Badge/readmodel/update-indicatie | Meestal niet-kritiek en herstelbaar. |
| Externe side effects zoals e-mail, PDF-generatie of SignalR | Buiten de database-transactie; idempotent, retrybaar of met beheerbare failed-status. |
| Multi-domain job met all-or-nothing karakter | Expliciet transactioneel of compensatiebeleid vastleggen vóór implementatie. |
Een mutatie is kritiek wanneer de gebruikersactie zonder die mutatie leidt tot een ongeldige, onbereikbare, misleidende of niet-herstelbare functionele toestand. Bij twijfel wordt de stap als kritiek behandeld totdat het tegendeel is onderbouwd. Een externe message broker, distributed transaction coordinator of apart workflowplatform wordt hiervoor niet geïntroduceerd in de V1.0-baseline.
Voorbeeld relatie-uitnodiging:
Kritiek:
- RelationshipInvitation aanmaken
- SystemMessage aanmaken wanneer dit de primaire ingang voor acceptatie is
- RelationshipEvent vastleggen indien dit de formele auditbron is
Niet kritiek of mogelijk retrybaar:
- badge bijwerken
- frontpage-teller verversen
- realtime indicatie versturen
Voorbeeld oefening delen:
Kritiek:
- SharedExercise-record aanmaken
- ontvanger en relatiecontext valideren
- snapshot van gedeelde context vastleggen
Afhankelijk van functionele ingang:
- SystemMessage is kritiek wanneer het de enige ingang is
- SystemMessage is retrybaar wanneer gedeelde oefeningen ook via een apart overzicht bereikbaar zijn
7.23 Logging en correlatie bij databaseacties
Databaseacties moeten herleidbaar zijn binnen HTTP-requests, domeinservices en jobs. Dit is extra belangrijk bij meerdere DbContexts, cross-module workflows en retrybare jobuitvoering.
| Gegeven | Gebruik |
|---|---|
CorrelationId | Koppelt request, domeinactie, job en logging aan elkaar. |
UserId | Actor wanneer een ingelogde gebruiker de actie start. |
RoleContext | Snapshot van actieve rolcontext bij gebruikersactie. |
JobId | Koppeling naar scheduling-uitvoering. |
JobType | Functionele identificatie van het jobtype. |
AttemptNumber | Retrypoging binnen jobuitvoering. |
EntityType en EntityId | Functionele verwijzing in logging waar passend. |
StartedAtUtc en CompletedAtUtc | Timing van actie of job. |
ErrorCode en foutcontext | Analyseerbare foutregistratie zonder gevoelige payloads. |
Logging mag geen credentials, tokens, wachtwoorden of gevoelige payloads vastleggen. Voor technische foutanalyse worden correlatiegegevens, domeinreferenties en veilige foutcodes gebruikt.
7.24 Securitygerelateerde databaseregels
Er komt geen apart securityproject in de eerste baseline, maar securitygerelateerde databasekeuzes worden wel expliciet gemaakt.
| Onderwerp | Regel |
|---|---|
| Credentials | Niet opslaan in OefenHub-database; identity provider is bron. |
| Tokens | Niet persistent opslaan in domeintabellen. |
| Access denied logging | Alleen veilige contextgegevens, geen resultaatinhoud of gevoelige payload. |
| Verdachte toegangspogingen | Logbaar via technische logging of passend domein, met correlatie. |
| PII in snapshots | Alleen opnemen wanneer functioneel nodig en onder anonimisering brengen. |
| Secrets/configuratie | Via environment/appsettingsbinding, niet in seeddata of migrations. |
| Databasegebruiker | Eerste baseline gebruikt één applicatieconnectionstring; verdere splitsing is een mogelijke verhardingsmaatregel via expliciet Technisch Ontwerp-besluit. |
Securityheaders, HSTS, CSP, rate limiting en request-pipelinebeveiliging worden technisch uitgewerkt in hoofdstuk 20. Dit hoofdstuk beperkt zich tot database- en persistenceconsequenties.
7.25 Validatiechecklist per datamodelwijziging
Bij iedere structurele datamodelwijziging moet minimaal onderstaande checklist worden doorlopen.
| Controle | Vraag |
|---|---|
| Bron | Staat de functionele betekenis in database-informatie, Functioneel Ontwerp of Software Requirements Specification? |
| Module-eigenaar | Welk project bezit de tabel of wijziging? |
| Schema | Past de tabel in het schema van die module? |
| DbContext | Hoort de entity in precies één module-DbContext? |
| Relatievorm | Is de relatie hard FK, soft link, soft link + snapshot of applicatielogica? |
| Snapshot | Is historische naamgeving/context nodig? |
| Anonimisering | Bevat de tabel persoonsgegevens of persoonsherleidbare snapshots? |
| Delete behavior | Is hard delete, soft delete, deactiveren of bewaren passend? |
| Index | Zijn querypatronen en autorisatiechecks ondersteund? |
| Unique constraint | Moet functionele uniciteit technisch worden afgedwongen? |
| Seeddata | Is seeddata nodig en idempotent? |
| Migration | Zit de migration in de juiste modulemap? |
| Logging | Is de wijziging herleidbaar via correlation/audit waar nodig? |
| Tests | Zijn module-, integratie- of architecture tests nodig? |
| Documentatie-impact | Moeten database-informatie, ERD, Technisch Ontwerp, Functioneel Ontwerp, Software Requirements Specification, schermdocumentatie of usecases worden bijgewerkt? |
7.26 Implementatieverificaties
De volgende punten worden vóór of tijdens implementatie expliciet geverifieerd en waar nodig verwerkt in het besluitenregister:
| Punt | Vervolg |
|---|---|
| EF Core migration history per DbContext | Verifiëren dat iedere persistente DbContext __EFMigrationsHistory in het eigen schema gebruikt en dat lege-database- en upgradepadtests slagen. |
| TickerQ schema-inrichting | Controleren hoe TickerQ-tabellen en dashboardconfiguratie schema-specifiek worden ingesteld. |
| Cross-module hard FK uitzonderingen | Per eventuele uitzondering expliciet motiveren en vastleggen. |
| Snapshotvelden per tabel | In database-informatie controleren waar snapshots al zijn vastgelegd en waar aanvulling nodig is. |
| Seedstrategie per module | Per module bepalen welke seedtypes nodig zijn en wanneer zij lopen. |
| Database backup voor migrations | Uitwerken in beheer- en operatiehoofdstuk. |
| Materialisatie van readmodels | Uitwerken in hoofdstuk 17 wanneer concrete performancekeuzes bekend zijn. |