Skip to main content

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:

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.

PrincipeRegel
Eén databaseAlle modules gebruiken dezelfde primaire relationele database.
Eén connectionstringDe applicatie gebruikt in de eerste baseline één applicatieconnectionstring.
Schema per moduleEen module met persistente data heeft één eigen databaseschema.
DbContext per moduleEen module met persistente data heeft maximaal één eigen DbContext.
Geen generieke auditmoduleAudit- en historytabellen blijven bij het domein dat de bronmutatie bezit.
Geen generiek securityschemaSecurityconfiguratie en logging worden verdeeld over Web, Infrastructure, Identity en Authorization, tenzij via een expliciet Technisch Ontwerp-besluit een zelfstandige securitymodule wordt toegevoegd.
Geen centrale readmodelmoduleModule-eigen readmodels blijven bij de module. Samengestelde UI-viewmodels worden via publieke query-services opgebouwd.
Database-informatie is bronHet 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.

ProjectDbContextSchemaOpmerking
OefenHub.IdentityIdentityDbContextidentityInterne accounts, profielkoppeling met externe identity provider en accountlifecycle.
OefenHub.AuthorizationAuthorizationDbContextauthorizationApplicatierollen, rolcontext, policies en autorisatiegerelateerde opslag indien nodig.
OefenHub.RelationshipsRelationshipsDbContextrelationshipsRelaties, uitnodigingen, relatie-events en relatieafhankelijke brondata.
OefenHub.CatalogCatalogDbContextcatalogNiveaus, categorieën, oefeningen, modulekoppeling en catalogushistorie.
OefenHub.ExerciseModuleHostModulesDbContext indien nodigmodulesTechnische modulemetadata, modulebeschikbaarheid en modulemigratiegegevens.
OefenHub.PracticePracticeDbContextpracticeOefenruns, voortgang, gedeelde oefeningen, resultaten en historische runcontext.
OefenHub.CommunicationCommunicationDbContextcommunicationInterne systeemberichten, privéberichtthreads, systeemberichttemplates en communicatiehistorie.
OefenHub.MailMailDbContextmailApplicatiegestuurde externe e-mail, mailtemplates, templatehistory en mailverzendpogingen.
OefenHub.SupportSupportDbContextsupportMeldingen, tickets, ticketdiscussie, sluitingen, heropenverzoeken en supporthistorie.
OefenHub.LiveMonitoringLiveMonitoringDbContextliveLive-meekijksessies, live-audit en live-gerelateerde status waar persistent nodig.
OefenHub.AdminAdminDbContextadminBeheerbare content, vaste pagina’s, footercontent, URL-records, popups, site-instellingen, systeemnotificaties, featuretoggles en beheerlogs.
OefenHub.ReportingReportingDbContext indien nodigreportingPersistente rapportage- of exportmetadata wanneer dit via een expliciet Technisch Ontwerp-besluit noodzakelijk wordt.
OefenHub.SchedulingSchedulingDbContextschedulingTickerQ-persistence, jobstatus, technische jobhistorie en retrybare verwerking.
OefenHub.WebgeengeenUI, routing, pipeline en page composition; geen eigen datalaag.
OefenHub.Infrastructuregeen standaardcontextgeen standaardschemaTechnische adapters en infrastructuurconfiguratie; alleen een schema bij expliciet vastgelegde noodzaak.
OefenHub.SharedKernelgeengeenGedeelde generieke types; geen persistente eigenaarschap.
OefenHub.Modules.AbstractionsgeengeenContracten voor technische oefenmodules.
OefenHub.Modules.<ModuleCategory>.<ModuleName>geen standaardcontextgeen standaardschemaConcrete 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.

BeheergebiedTabellen of technische bronTechnische regel
PopupbeheerPopupDetails, PopupDetailsHistory, PopupDetailsHistoryItemsNieuwe popupkeys ontstaan via code en migratie. Beheer wijzigt alleen toegestane content- en presentatievelden.
FeaturetogglesSiteFeatureToggles, SiteFeatureToggleHistoryAlleen expliciet togglebare functies worden opgenomen. Verplichte kernfunctionaliteit wordt niet als featuretoggle gemodelleerd.
SysteeminstellingenSystemSettingsNieuwe keys ontstaan via code en migratie. De beheerinterface wijzigt alleen bestaande keys binnen datatype- en validatiegrenzen.
ContentblokkenContentBlocks, ContentBlockHistoryTekstuele content is beheerbaar; layout, blokpositie en renderstructuur blijven codegedreven.
URL-recordsSiteLinks, SiteLinkHistoryInterne en externe URL’s worden server-side gevalideerd. In gebruik zijnde links worden niet hard verwijderd.
FooterindelingFooterSections, FooterLinkAssignments, FooterLinkAssignmentHistoryFooterplaatsingen zijn contextgebonden en volgen de vaste footerstructuur.
SysteemnotificatiesSiteNotifications, SiteNotificationHistoryFrontpage-overlays zijn gescheiden van mailbox-systeemberichten en popupregister-popups.
BeheerloggingManagementLogEntries of domeinspecifieke historyBeheeracties 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.

ObjectConventieVoorbeeld
Database schemakleine letters, Engels, betekenisvolpractice, communication, support
TabelPascalCase, meervoud of bestaande database-informatie-conventieExerciseRuns, SystemMessages, TicketClosures
KolomPascalCaseCreatedAtUtc, UserId, CategoryNameSnapshot
Primary keyId, tenzij database-informatie bewust anders bepaaltId
Foreign-keyachtige waarde<Object>IdUserId, ExerciseRunId
Snapshotveld<Naam>Snapshot of duidelijke contextnaamUserDisplayNameSnapshot
DbContext<ModuleName>DbContextPracticeDbContext
Entity configuration<EntityName>ConfigurationExerciseRunConfiguration
Migrationtimestamp plus korte Engelstalige beschrijving20260601090000_AddExerciseRunSnapshots
IndexIX_<Table>_<Columns>IX_ExerciseRuns_UserId_CompletedAtUtc
Unique constraintUX_<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/
MapBetekenis
ContractsPublieke module-ingang voor andere modules.
Contracts/ModelsDTO's die onderdeel zijn van publieke modulecontracten.
DataModule-eigen DbContext en databasevertaling.
Data/EntitiesEF Core entities; standaard internal.
Data/ConfigurationsIEntityTypeConfiguration<T>-configuraties.
Data/MigrationsEF Core migrations voor de module-DbContext.
ModelsModule-interne DTO's en modellen.
Models/ReadModelsModule-eigen readmodels voor queryresultaten.
ServicesImplementatie van module-usecases, command-services en query-services.
Services/InterfacesInterne service-interfaces, niet bedoeld als publieke modulecontracten.
EventsModule-interne of expliciet gepubliceerde application events.
HelpersKleine module-lokale helpers zonder domeinbeslissingen.
ExtensionsRegistratie- 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.

RegelToelichting
Eén schema per DbContextmodelBuilder.HasDefaultSchema("practice") of expliciete schema-configuratie per entity.
Geen cross-module DbSetsPracticeDbContext bevat geen Users, SystemMessages of Tickets.
Entities standaard internalAndere modules mogen niet rechtstreeks entitytypen gebruiken.
DbContext standaard module-internAlleen registratie en module-services gebruiken de context.
Configuraties per entityFluent API-configuratie staat in Data/Configurations.
Geen businesslogica in DbContextDbContext bevat mapping en persistenceconfiguratie, geen domeinbeslissingen.
Geen UI-afhankelijkhedenDbContext 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.

OnderwerpRegel
EntitynaamSluit aan bij tabelnaam zonder schema.
Primary keyId als technische sleutel, tenzij database-informatie anders bepaalt.
NullableDatabase-nullability volgt functionele betekenis uit database-informatie.
UTC-tijdveldenTijdvelden die proces- of auditmomenten vastleggen gebruiken UTC.
Soft deleteAlleen toepassen waar database-informatie of domeinregels historische zichtbaarheid vereisen.
Rowversion/concurrencyGebruiken bij records met relevante gelijktijdige wijzigingsrisico's.
NavigatiepropertiesBinnen module toegestaan; over modulegrens standaard vermijden.
Owned typesAlleen gebruiken voor duidelijke value objects binnen dezelfde module.
JSON/base64-payloadsAlleen 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.

RelatietypeStandaard technische vormVoorbeeld
Binnen dezelfde module zonder historische uitzonderingHarde FKsupport.TicketClosures.TicketId naar support.Tickets.Id
Binnen dezelfde module met historische contextFK + snapshotSluiting met actuele relatie plus tekstuele statusmomentopname
Over modulegrens zonder historische contextSoft linkpractice.ExerciseRuns.UserId verwijst logisch naar identity.Users.Id
Over modulegrens met historische contextSoft link + snapshotpractice.ExerciseRuns.UserId plus UserDisplayNameSnapshot
Polymorfe of beperkte domeinverwijzingApplicatielogica + validatiecommunication.SystemMessages.EntityType + EntityId
Audit/history naar actorSoft link + actor snapshotadmin.ManagementLogEntries.ActorUserId plus naam/rolsnapshot
Uitzonderlijke cross-module harde afhankelijkheidExpliciet besluit nodigAlleen 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.

VraagAls het antwoord ja isGevolg
Liggen beide tabellen in hetzelfde schema en dezelfde DbContext?JaHarde FK is het uitgangspunt.
Moet het doelrecord historisch kunnen verdwijnen, anonimiseren of inactief worden zonder bronrecord te breken?JaGebruik snapshot en vermijd cascade delete.
Is de relatie over modulegrenzen heen?JaGebruik standaard soft link of soft link + snapshot.
Is het doelrecord de enige geldige bron voor actuele verwerking?JaOverweeg expliciete validatie via modulecontract.
Is de relatie polymorf of afhankelijk van EntityType?JaGebruik applicatielogica, allowlist en autorisatiecontrole.
Moet rapportage oude naamgeving tonen?JaSnapshot verplicht.
Zou een hard FK de accountlifecycle, anonimisering of migratie blokkeren?JaGeen hard FK, tenzij expliciet besluit.
Is een cross-module hard FK toch noodzakelijk?JaLeg motivatie, eigenaar, delete behavior en migratie-impact vast.

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.

AspectRegel
VulmomentVastleggen op het moment waarop de historische context ontstaat.
ImmutabilitySnapshotvelden worden na vastlegging niet stilzwijgend bijgewerkt.
FallbackSnapshot wordt gebruikt wanneer actuele brondata ontbreekt, gewijzigd is of niet meer passend is.
PersoonsgegevensSnapshotwaarden met persoonsgegevens vallen onder anonimisering en retentiebeleid.
PDF/exportExport gebruikt historische runcontext en snapshots waar relevant.
ModulemigratieMigraties mogen historische snapshots niet herschrijven tenzij expliciet als herstelactie besloten.
BeheerwijzigingenLatere 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:

DomeinMogelijke snapshots
Practiceniveau-, categorie-, oefening-, module- en gebruikersnaam op runmoment.
Communicationafzendernaam, rolcontext of systeemreferentie op verzendmoment.
Supportmeldernaam, rolcontext, technische pagina- en browsersnapshot op meldmoment.
Adminactorrol, oude/nieuwe waarde en reden bij beheeractie.
LiveMonitoringviewerrolnaam 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.

SituatieVoorkeur
Functionele relatie beëindigenSoft-deactivatie met datum, actor en reden indien nodig.
Account verwijderenAnonimisering en beëindiging van toegang, geen generiek hard delete van historie.
Relatie beëindigenIsActive = false of vergelijkbare lifecyclevelden.
Ticket sluitenSluitregistratie in TicketClosures, geen verwijderen van ticket.
Privéthread verwijderen uit mailboxParticipantgebonden zichtbaarheid aanpassen, thread niet hard verwijderen.
Tijdelijke testdataHard delete of cleanup toegestaan volgens jobbeleid.
Tijdelijke PDF/exportbestandenPeriodieke cleanup toegestaan; brondata blijft database.
Readmodels/cacheHerbouwbaar; 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 waardelijstTechnische aanpak
Stabiele technische enumC# enum of gesloten value object, met expliciete databaseconversie.
Beheerbare referentiedataTabel met idempotente seeddata.
Functionele status met lifecycleTabel of gesloten enum afhankelijk van wijzigingsbehoefte.
UI-labelsNiet als bron in enumcode; labels via applicatielaag of beheerbare content waar passend.
ModulecontractwaardenIn OefenHub.Modules.Abstractions of modulecontracten vastleggen.

Voorbeelden:

DomeinWaardelijstVerwachte aanpak
Supportticketstatussengesloten set of referentietabel volgens database-informatie.
Supportafsluitstatussenreferentiedata met idempotente seeddata.
Communicationsysteembericht-entitytypesgesloten set met allowlist en applicatielogica.
Relationshipsuitnodigingsstatussengesloten domeinset.
Catalogcategoriehistorie-actietypesgesloten 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.

SeedtypeVoorbeeldGedrag
Initiële technische seedstandaardrolcodes, basisstatussenAlleen aanmaken wanneer ontbrekend.
Referentiedataticketstatussen, afsluitstatussenIdempotent synchroniseren op sleutel/code.
Beheerbare content en templatespopupdefinities, contentblokken, footerrecords, URL-records, systeeminstellingen, featuretoggles, systeemnotificaties en systeemberichttemplatesInitieel vullen wanneer ontbrekend; bestaande beheerwijzigingen niet stilzwijgend overschrijven.
Modulemetadatageregistreerde oefenmodulesVia modulehost/catalogusproces en idempotente herkenning.
Development datatestgebruikers, voorbeeldrunsAlleen in development of testomgeving.
Testdataintegratie- en acceptatietestsNiet 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.

ObjectgroepEigenaarStabiele sleutelSeedgedragBeheer na seed
PopupregisterAdminPopupKeyAanmaken 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.
SysteemberichttemplatesCommunicationTemplateKeyAanmaken 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.
SysteeminstellingenAdminSettingKeyAanmaken 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.
FeaturetogglesAdminFeatureKeyAanmaken wanneer de key ontbreekt voor expliciet togglebare functionaliteit.Beheer mag de togglewaarde en toegestane beheerbare metadata wijzigen. De technische featurekey en betekenis blijven codegedreven.
SysteemnotificatiesAdminNotificationKey of vaste technische sleutel waar van toepassingAlleen vaste of initiële notificatiedefinities seeden wanneer zij ontbreken.Planning, inhoud en actiefstatus zijn beheerbaar binnen autorisatie- en validatiegrenzen.
Content- en linkblokkenAdminBlockKey, LinkKey of vaste plaatsingskeyInitieel 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:

  1. Eerst worden schema-migrations toegepast; daarna pas seedlogica.
  2. De seed zoekt op stabiele technische key, niet op label of weergavetekst.
  3. Ontbrekende records worden aangemaakt met veilige defaults.
  4. Bestaande records worden niet overschreven wanneer zij beheerbaar zijn.
  5. Wanneer een technische definitie wijzigt, gebeurt dat via expliciete migration, backfill of beheeractie met history/audit, niet via stille reseed.
  6. In productie wordt beheerbare seeddata niet bij iedere applicatiestart blind uitgevoerd.
  7. Seeds bevatten geen secrets, persoonsgegevens, tokens of omgeving-specifieke productiewaarden.
  8. 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.

RegelToelichting
Migrations per moduleIedere DbContext heeft eigen migration history en migrationbestanden.
Schema-eigenaarschapEen migration wijzigt alleen het schema van de eigen module, tenzij expliciet anders besloten.
Geen verborgen cross-module DDLEen migration maakt niet stilzwijgend tabellen of constraints in andere schema's.
Volgorde explicietDeployment voert migrations in een vaste, herhaalbare volgorde uit.
Seed na schemaSeeddata wordt pas uitgevoerd nadat schema-migrations succesvol zijn.
IdempotentieSeeds en technische datawijzigingen zijn herhaalbaar zonder dubbele records.
RollbackbeleidRollback wordt per release beoordeeld en niet blind aan EF down-migrations overgelaten.
Backup voor productieProductiemigraties 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.

KeuzeBaseline
Eén centrale history zonder contextonderscheidNiet toegestaan.
Per DbContext eigen historytabelnaam in één schemaAlleen toegestaan wanneer het databaseplatform of providerbeperking history per schema onmogelijk maakt.
Per DbContext history in eigen schemaVerplicht als voorkeursinrichting.
Handmatig bijhouden buiten EFNiet gebruiken zolang EF Core de historytabel kan beheren.

De standaardnaam van de historytabel blijft __EFMigrationsHistory. De scheiding zit in het schema. Voorbeelden:

DbContextSchemaHistorytabel
IdentityDbContextidentityidentity.__EFMigrationsHistory
AuthorizationDbContextauthorizationauthorization.__EFMigrationsHistory
PracticeDbContextpracticepractice.__EFMigrationsHistory
CommunicationDbContextcommunicationcommunication.__EFMigrationsHistory
SchedulingDbContextschedulingscheduling.__EFMigrationsHistory
ReportingDbContextreportingAlleen wanneer ReportingDbContext daadwerkelijk persistent wordt gebruikt.

Technische inrichting:

options.UseSqlServer(
connectionString,
sql => sql.MigrationsHistoryTable("__EFMigrationsHistory", moduleSchema));

Regels:

  1. iedere module-DbContext configureert expliciet de eigen historytabel in het eigen schema;
  2. migrations blijven in de module-eigen Data/Migrations-map;
  3. een migration schrijft geen historyrijen in een ander moduleschema;
  4. de deploymentvolgorde uit hoofdstuk 7.16 blijft leidend voor het uitvoeren van migrations;
  5. 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.

QuerypatroonMogelijke indexrichting
Geschiedenis per gebruiker en periodeUserId, CompletedAtUtc, ExerciseId
Ouder-/voogdresultaten per kindStudentUserId, CompletedAtUtc
Docentgeschiedenis binnen niveaucontextLevelId, StudentUserId, CompletedAtUtc
Openstaande relatie-uitnodigingenToUserId, ToEmailNormalized, Status, ExpiresAtUtc
Ongelezen berichtenRecipientUserId, ReadAtUtc, SentAtUtc
Tickets per statusStatus, UpdatedAtUtc
Wacht-op-mij indicatorCreatedByUserId, Status
Actieve live sessiesStudentUserId, ExerciseRunId, EndedAtUtc
Scheduler pending jobsStatus, NextAttemptAtUtc, JobType

Unique constraints worden gebruikt waar zij echte functionele uniciteit afdwingen binnen het eigen domein. Voorbeelden:

DomeinMogelijke unieke constraint
Relationshipsgeen dubbele actieve relatie voor dezelfde combinatie van gebruikers, relatietype en rolcontext.
Relationshipsgeen conflicterende openstaande uitnodiging voor dezelfde combinatie waar dat functioneel verboden is.
Catalogtechnische modulecode of categoriekey waar die code uniek moet zijn.
Communicationtemplatekey of popupkey binnen het eigen beheerbereik.
Schedulingidempotency 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.

PayloadtypeEigenaarRegel
Oefeningconfiguratiecatalog en modulecontractGeneriek opgeslagen, modulespecifiek gevalideerd.
Vraag-/antwoordpayloadpractice en modulecontractBron voor modulespecifieke reconstructie.
VoortgangsdetailspracticeServer-side opgeslagen na bevestigde stappen.
PDF-renderdatamodulecontract en practiceHistorische 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.

ReadmodeltypePlaatsBronstatus
Module-eigen queryresultaatModels/ReadModels binnen moduleAfgeleid uit eigen brondata.
Samengesteld UI-viewmodelOefenHub.Web/ViewModels of PageCompositionSamengesteld via publieke query-services.
Fysiek gematerialiseerd readmodelSchema van eigenaar-moduleHerbouwbaar of met expliciet herstelbeleid.
Badge/tellerEigenaar van de tellerdefinitieAfgeleid; geen autorisatiebron.
CacheTechnische laagMag 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.

OnderdeelRegel
JobopslagPersistent in het scheduling schema.
JoblifecycleEigendom van OefenHub.Scheduling na succesvolle jobaanmaak.
JobuitvoeringDomeinactie via publiek contract van de eigenaar-module.
RetrybeleidPer jobtype configureerbaar, begrensd en gelogd.
FoutstatusFailed jobs blijven beheerbaar zichtbaar.
CorrelationCorrelationId wordt door de volledige jobketen doorgegeven.
TickerQ-dashboardAlleen 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.

SituatieRichting
Alleen eigen modulebrondataModule-eigen transactie.
Meerdere kritieke mutaties die samen één functionele uitkomst vormenAtomaire uitvoering binnen dezelfde applicatieflow en database wanneer technisch passend; anders expliciete failed-status of compensatiebeleid.
Niet-kritieke naverwerkingRetrybaar via TickerQ-job, pending action, cache-invalidatie of readmodelrebuild.
Systeembericht als enige functionele ingangBeschouwen als kritiek binnen die workflow.
Badge/readmodel/update-indicatieMeestal niet-kritiek en herstelbaar.
Externe side effects zoals e-mail, PDF-generatie of SignalRBuiten de database-transactie; idempotent, retrybaar of met beheerbare failed-status.
Multi-domain job met all-or-nothing karakterExpliciet 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.

GegevenGebruik
CorrelationIdKoppelt request, domeinactie, job en logging aan elkaar.
UserIdActor wanneer een ingelogde gebruiker de actie start.
RoleContextSnapshot van actieve rolcontext bij gebruikersactie.
JobIdKoppeling naar scheduling-uitvoering.
JobTypeFunctionele identificatie van het jobtype.
AttemptNumberRetrypoging binnen jobuitvoering.
EntityType en EntityIdFunctionele verwijzing in logging waar passend.
StartedAtUtc en CompletedAtUtcTiming van actie of job.
ErrorCode en foutcontextAnalyseerbare 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.

OnderwerpRegel
CredentialsNiet opslaan in OefenHub-database; identity provider is bron.
TokensNiet persistent opslaan in domeintabellen.
Access denied loggingAlleen veilige contextgegevens, geen resultaatinhoud of gevoelige payload.
Verdachte toegangspogingenLogbaar via technische logging of passend domein, met correlatie.
PII in snapshotsAlleen opnemen wanneer functioneel nodig en onder anonimisering brengen.
Secrets/configuratieVia environment/appsettingsbinding, niet in seeddata of migrations.
DatabasegebruikerEerste 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.

ControleVraag
BronStaat de functionele betekenis in database-informatie, Functioneel Ontwerp of Software Requirements Specification?
Module-eigenaarWelk project bezit de tabel of wijziging?
SchemaPast de tabel in het schema van die module?
DbContextHoort de entity in precies één module-DbContext?
RelatievormIs de relatie hard FK, soft link, soft link + snapshot of applicatielogica?
SnapshotIs historische naamgeving/context nodig?
AnonimiseringBevat de tabel persoonsgegevens of persoonsherleidbare snapshots?
Delete behaviorIs hard delete, soft delete, deactiveren of bewaren passend?
IndexZijn querypatronen en autorisatiechecks ondersteund?
Unique constraintMoet functionele uniciteit technisch worden afgedwongen?
SeeddataIs seeddata nodig en idempotent?
MigrationZit de migration in de juiste modulemap?
LoggingIs de wijziging herleidbaar via correlation/audit waar nodig?
TestsZijn module-, integratie- of architecture tests nodig?
Documentatie-impactMoeten 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:

PuntVervolg
EF Core migration history per DbContextVerifiëren dat iedere persistente DbContext __EFMigrationsHistory in het eigen schema gebruikt en dat lege-database- en upgradepadtests slagen.
TickerQ schema-inrichtingControleren hoe TickerQ-tabellen en dashboardconfiguratie schema-specifiek worden ingesteld.
Cross-module hard FK uitzonderingenPer eventuele uitzondering expliciet motiveren en vastleggen.
Snapshotvelden per tabelIn database-informatie controleren waar snapshots al zijn vastgelegd en waar aanvulling nodig is.
Seedstrategie per modulePer module bepalen welke seedtypes nodig zijn en wanneer zij lopen.
Database backup voor migrationsUitwerken in beheer- en operatiehoofdstuk.
Materialisatie van readmodelsUitwerken in hoofdstuk 17 wanneer concrete performancekeuzes bekend zijn.