Skip to main content

Applicatielagen, projectstructuur en dependency-richting

3.1 Doel van dit hoofdstuk

Dit hoofdstuk legt vast hoe projecten binnen de OefenHub-solution intern worden opgebouwd, welke lagen en mappen per project worden gebruikt, welke afhankelijkheden zijn toegestaan en hoe modulegrenzen technisch worden bewaakt.

Dit hoofdstuk werkt de solution-opbouw uit naar concrete projectregels, mapstructuren, contractafspraken, dependency-richting, query-services, command-services, events, scheduling-ingangen en frontendafbakening.

Belangrijke inputbronnen en detailhoofdstukken zijn:

3.2 Ontwerpprincipes

PrincipeRegel
Modulegrens boven gemakEen module mag niet rechtstreeks interne classes, entities, DbContexts of repositories van een andere module gebruiken.
Publieke ingang expliciet makenAlles wat door andere modules gebruikt mag worden, staat onder Contracts of wordt via een expliciet publiek contract aangeboden.
Implementatie standaard internImplementatieclasses, EF Core entities, DbContexts, configuraties en interne services zijn standaard internal, tenzij er een expliciete publieke reden is.
Web bevat geen domeinlogicaOefenHub.Web bevat routing, componenten, formulieren, viewmodels en page composition, maar geen businesslogica en geen directe datatoegang.
Eén project, maximaal één DbContextEen persistent moduleproject heeft maximaal één eigen DbContext en daarmee maximaal één eigen databaseschema.
Query's via leespoortenCross-module leesvragen lopen via query-services/readers, niet via directe databasequeries op andermans tabellen.
Commands via services/facadesCross-module mutaties lopen via publieke command-services/facades of workflow-ingangen.
Geen externe message broker in baselineModulecommunicatie blijft binnen dezelfde applicatie. TickerQ wordt gebruikt voor geplande of retrybare verwerking, niet RabbitMQ/Kafka.
DRY zonder shared-kernel-vervuilingGedeelde types gaan alleen naar SharedKernel wanneer zij aantoonbaar generiek zijn en niet bij één module horen.
Testbaarheid per moduleModules moeten zelfstandig testbaar zijn via eigen testprojecten en contracten.

3.3 Logische lagen

Binnen OefenHub worden lagen niet als aparte Domain, Application en Persistence projecten per module aangemaakt. In plaats daarvan bevat elk moduleproject zelf een compacte interne laagindeling.

LaagFysieke plekVerantwoordelijkheid
UI/compositieOefenHub.WebRouting, Blazor/Razor pages, componenten, layouts, formulieren, viewmodels en page composition.
Publieke module-ingang<Module>/ContractsInterfaces, contract-DTO's, contract-enums en eventueel contract-events die andere modules mogen gebruiken.
Application/service-laag<Module>/ServicesUsecase-afhandeling, command-services, query-services, validatiecoördinatie en workflowstappen binnen de module.
Domeinlogica<Module>/Services, <Module>/Models, module-interne classesDomeinregels, statusovergangen, berekeningen en beslislogica van de module.
Datatoegang<Module>/DataModule-eigen DbContext, entities, EF Core-configuraties en migrations.
Technische infrastructuurOefenHub.Infrastructure, OefenHub.Web, module ExtensionsLoggingintegratie, configuratiebinding, middleware-registratie, adapters en technische hostintegratie.
SchedulingOefenHub.SchedulingTickerQ-integratie, jobregistratie, jobstatussen, retries, joblogging en interne jobbeheerinterface.

Deze indeling voorkomt een groot aantal kleine projecten per module, maar behoudt wel duidelijke interne grenzen.

3.4 Dependency-richting op hoofdlijn

De dependency-richting loopt van buiten naar binnen en van host/compositie naar domeincontracten. Domeinmodules mogen niet afhankelijk worden van de UI-host.

De pijlen betekenen projectreferenties of toegestane contractafhankelijkheden. Ze betekenen niet dat implementatieclasses of entities van het doelproject vrij gebruikt mogen worden.

3.5 Toegestane projectafhankelijkheden

VanMag referencenMag niet referencen
OefenHub.WebPublieke contracts van modules, SharedKernel, infrastructuurregistratie, schedulingregistratie.Module-interne entities, module-interne DbContexts, directe SQL/datatoegang.
DomeinmoduleSharedKernel, publieke contracts van modules die functioneel nodig zijn, eventueel Infrastructure-abstractions indien bewust gekozen.OefenHub.Web, interne Data/Entities van andere modules, concrete UI-types.
OefenHub.SchedulingScheduling-contracten, publieke job-handlercontracten van domeinmodules, SharedKernel, TickerQ-integratie.Module-interne entities of services zonder contract, UI-types.
OefenHub.ExerciseModuleHostModules.Abstractions, modulemetadata-contracten, eventueel Catalog via publiek contract indien nodig.Concrete module-interne implementatie buiten het modulecontract.
Concrete oefenmoduleModules.Abstractions, beperkt SharedKernel.Web, Catalog, Practice, ExerciseModuleHost-internals, andere concrete moduleprojecten.
TestprojectHet project dat getest wordt, testhelpers en testinfrastructuur.Productieprojecten buiten het doel van de test, tenzij het een integratie- of architectuurtest is.

Wanneer een dependency nodig lijkt die niet in deze tabel past, moet eerst worden beoordeeld of het om een ontbrekend publiek contract, een verkeerd gekozen eigenaarschap of een echte architectuuruitzondering gaat.

3.6 Standaardstructuur voor domeinmoduleprojecten

Een domeinmodule gebruikt de volgende basisstructuur. Mappen worden alleen aangemaakt wanneer zij inhoud hebben; lege mappen zijn niet verplicht.

OefenHub.<ModuleName>/
Contracts/
Models/
Enums/
Events/

Data/
<ModuleName>DbContext.cs
Entities/
Configurations/
Migrations/

Models/
Commands/
Queries/
ReadModels/
Enums/

Services/
Interfaces/

Events/

Helpers/

Extensions/
MapDoelPubliek of intern
ContractsPublieke module-ingang voor andere modules. Bevat interfaces, contract-DTO's, contract-enums en eventueel contract-events.Publiek waar nodig.
Contracts/ModelsRequest-/responsemodellen die onderdeel zijn van publieke contracts.Publiek.
Contracts/EnumsEnums die onderdeel zijn van publieke contracts.Publiek.
Contracts/EventsEvents waarop andere modules mogen reageren of die buiten de module gebruikt mogen worden.Publiek, alleen indien nodig.
DataModule-eigen EF Core datatoegang. Bevat DbContext, entities, configuraties en migrations.Intern.
Data/EntitiesEF Core entities van de module.Intern.
Data/ConfigurationsEF Core entity configurations.Intern.
Data/MigrationsEF Core migrations voor de module-eigen DbContext.Intern/technisch.
ModelsModule-interne DTO's, commandmodellen, querymodellen, readmodels en enums.Intern, tenzij bewust anders.
Models/CommandsModellen voor interne command-afhandeling binnen de module.Intern.
Models/QueriesModellen voor interne query-afhandeling binnen de module.Intern.
Models/ReadModelsModule-eigen readmodels voor schermen, overzichten, tellers of exports waarvoor de module eigenaar is.Intern of via contract gepubliceerd.
Models/EnumsModule-interne enumwaarden.Intern.
ServicesImplementatie van command-services, query-services, validators, calculators en modulelogica.Intern, behalve expliciet publieke contractimplementaties via DI.
Services/InterfacesInterne interfaces voor module-eigen serviceverdeling. Deze map is organisatorisch en hoeft niet als aparte namespace zichtbaar te zijn.Intern.
EventsModule-interne events of eventmodellen.Intern, tenzij verplaatst naar Contracts/Events.
HelpersKleine lokale hulpfuncties zonder domeineigenaarschap.Intern.
ExtensionsExtension methods, meestal voor dependency injection of lokale technische registratie.Publiek wanneer hostregistratie dit vereist.

Naamgeving van DTO's

DTO-klassen die als data-transferobject worden gebruikt krijgen expliciet de suffix Dto. Dit geldt voor publieke contract-DTO's, module-interne command-/querymodellen wanneer zij als DTO worden gebruikt en web-/exportmodellen die als transportmodel over een laag- of modulegrens gaan. Voorbeelden zijn PracticeResultDetailDto, PracticePdfExportDto en moduleconfiguratie-DTO's.

Readmodels, entities, optionsclasses, validators, commands en domeinspecifieke value objects krijgen niet automatisch de suffix Dto; zij volgen hun eigen naamgevingsconventie. De suffix is bedoeld om transportmodellen herkenbaar te houden en te voorkomen dat database-entiteiten of domeinobjecten als DTO worden gebruikt.

3.7 Contracts als publieke module-ingang

Contracts is de enige standaardplek waar andere modules op mogen bouwen. Een contract beschrijft wat de module aanbiedt, niet hoe de module dit intern realiseert.

Voorbeeld voor OefenHub.Relationships:

OefenHub.Relationships/
Contracts/
IRelationshipAccessReader.cs
IRelationshipInvitationService.cs
Models/
RelationshipAccessCheckRequest.cs
RelationshipAccessCheckResult.cs
Enums/
RelationshipAccessDecision.cs

Andere modules mogen dan bijvoorbeeld vragen:

var result = await relationshipAccessReader.CanShareExerciseAsync(request, cancellationToken);

Zij mogen niet rechtstreeks:

// Niet toegestaan buiten OefenHub.Relationships
relationshipsDbContext.UserRelationships.Where(...);

Contracten worden bewust klein gehouden. Een contract mag geen module-interne entitytypes, EF Core types of implementatiedetails lekken.

3.8 Public versus internal

De standaardregel is:

internal class ExerciseRun
{
}

voor entities en implementatieclasses.

Alleen contracttypes zijn publiek wanneer andere projecten ze moeten kunnen gebruiken:

public interface IExerciseRunResultReader
{
Task<ExerciseRunResultReadModel> GetCompletedRunAsync(
CompletedRunQuery query,
CancellationToken cancellationToken);
}

Implementatie blijft intern:

internal sealed class ExerciseRunResultReader : IExerciseRunResultReader
{
}

Dit maakt de gewenste modulegrens compile-time zichtbaar. Andere projecten kunnen het contract gebruiken, maar niet de interne implementatie of entities.

3.9 Data-laag binnen modules

Een persistent moduleproject heeft maximaal één eigen DbContext. De DbContext beheert uitsluitend tabellen binnen het eigen schema van de module.

Voorbeeld:

OefenHub.Practice/
Data/
PracticeDbContext.cs
Entities/
ExerciseRun.cs
ExerciseRunProgress.cs
SharedExercise.cs
Configurations/
ExerciseRunConfiguration.cs
ExerciseRunProgressConfiguration.cs
SharedExerciseConfiguration.cs
Migrations/

De DbContext wordt niet rechtstreeks door OefenHub.Web gebruikt. Web roept modulecontracts of services aan; de module gebruikt intern haar eigen DbContext.

internal sealed class PracticeDbContext : DbContext
{
internal DbSet<ExerciseRun> ExerciseRuns => Set<ExerciseRun>();
internal DbSet<ExerciseRunProgress> ExerciseRunProgress => Set<ExerciseRunProgress>();
}

De exacte technische zichtbaarheid van DbSet-properties kan bij implementatie worden afgestemd op EF Core-beperkingen, maar het ontwerpuitgangspunt blijft dat DbContext en entities niet als cross-module API worden gebruikt.

3.10 Commands, queries en readmodels

Binnen OefenHub wordt onderscheid gemaakt tussen command-services en query-services.

TypeDoelVoorbeeld
Command-serviceVoert een mutatie of usecase uit.IRelationshipInvitationService.InviteGuardianAsync(...)
Query-service / readerLeest gegevens via een publieke module-ingang.IRelationshipAccessReader.HasActiveGuardianRelationAsync(...)
Readmodel-readerLeest een scherm-, teller-, export- of dashboardmodel.IExerciseRunResultReader.GetCompletedRunForPdfAsync(...)
Interne repository/queryOndersteunt de module-implementatie.Alleen binnen dezelfde module toegestaan.

Een query-service is dus geen externe message queue en geen los infrastructuurproduct. Het is een expliciete leespoort van een module.

Voorbeeld:

public interface IMessageBadgeReader
{
Task<MessageBadgeReadModel> GetBadgeAsync(
UserMessageContext context,
CancellationToken cancellationToken);
}

De implementatie mag intern CommunicationDbContext gebruiken. De aanroepende module of Web krijgt alleen het readmodel terug.

3.11 Module-eigen readmodels

Readmodels staan standaard bij de module die eigenaar is van de gegevens of de functionele uitlezing.

OefenHub.Practice/
Models/
ReadModels/
GuardianResultReadModel.cs
ExerciseRunHistoryReadModel.cs
CompletedRunPdfSourceModel.cs

Samengestelde UI-viewmodels staan niet in een centraal ReadModels project. Zij worden in OefenHub.Web samengesteld via publieke query-services.

Voorbeeld:

OefenHub.Web/
PageComposition/
TeacherFrontpageComposer.cs
ViewModels/
TeacherFrontpageViewModel.cs

De composer mag meerdere query-services aanroepen:

var catalogSummary = await catalogReader.GetTeacherCatalogSummaryAsync(context, cancellationToken);
var practiceSummary = await practiceReader.GetTeacherPracticeSummaryAsync(context, cancellationToken);
var ticketIndicator = await supportReader.GetActionIndicatorAsync(context, cancellationToken);

De composer mag geen DbContext of module-interne entity gebruiken.

3.12 Services en interne interfaces

Services bevat de implementatie van modulelogica. De map Services/Interfaces is toegestaan als organisatorische ordening voor interne interfaces. Deze map hoeft niet één-op-één in de namespace terug te komen.

Voorbeeld:

OefenHub.Practice/
Services/
StartExerciseRunService.cs
ExerciseRunResultReader.cs
ExerciseRunStatisticsCalculator.cs
Interfaces/
IExerciseRunStatisticsCalculator.cs

Interne interfaces zijn nuttig wanneer:

  • een service complex genoeg is om intern te splitsen;
  • een calculator of validator afzonderlijk getest moet worden;
  • implementaties vervangbaar moeten zijn binnen dezelfde module;
  • dependency injection binnen de module overzichtelijker wordt.

Interne interfaces worden niet automatisch publieke contracts.

3.13 Events binnen en tussen modules

Events kunnen intern of publiek zijn.

EventtypeLocatieGebruik
Module-intern event<Module>/EventsAlleen binnen dezelfde module.
Cross-module event<Module>/Contracts/EventsAndere modules mogen erop reageren of het via een dispatcher verwerken.
Scheduling payloadMeestal <Module>/Contracts/Models of scheduling-contractmodelWordt gebruikt om een job uitgesteld of gepland uit te voeren.

Voorbeeld intern event:

OefenHub.Practice/
Events/
ExerciseRunCompletedInternalEvent.cs

Voorbeeld publiek event:

OefenHub.Relationships/
Contracts/
Events/
RelationshipInvitationAcceptedEvent.cs

Cross-module events mogen geen EF Core entities bevatten. Zij bevatten alleen identifiers, snapshots of kleine contractmodellen die voor de ontvanger bedoeld zijn.

3.14 Cross-module workflows

Een cross-module workflow wordt geplaatst bij de module die functioneel eigenaar is van de gebruikersactie.

WorkflowEigenaarMogelijke betrokken modules
Relatie-uitnodiging aanmakenOefenHub.RelationshipsCommunication, eventueel Identity voor bestaande gebruikercontext.
Accountprovisioning en pending uitnodigingen koppelenOefenHub.IdentityRelationships, Communication.
Melding doorzetten naar docentOefenHub.SupportRelationships, Communication, eventueel Authorization.
Gedeelde oefening aanmakenOefenHub.PracticeRelationships, Communication.
Periodiek verlopen heropentermijnen verwerkenOefenHub.Scheduling triggert, Support voert domeinactie uitSupport, eventueel Communication voor vervolgactie indien functioneel nodig.

Er komt in de basis geen apart OefenHub.Workflows project. Als toekomstige workflows ontstaan zonder duidelijke domeineigenaar of met veel domeinoverstijgende coördinatie, kan een apart workflowproject via technisch ontwerpbesluit worden overwogen.

3.15 Transaction boundaries per workflow

Transaction boundaries worden niet generiek afgeleid uit het type stap. Per workflow wordt bepaald welke mutaties onderdeel zijn van de functionele transactie.

VraagGevolg
Leidt falen van deze stap tot een ongeldige of halve functionele toestand?Stap hoort bij de kritieke transactie.
Is deze stap de enige praktische ingang voor een gebruiker?Stap is waarschijnlijk kritiek.
Kan de stap veilig opnieuw worden uitgevoerd zonder dubbel effect?Stap kan mogelijk retrybaar zijn.
Is dubbele uitvoering schadelijk?Idempotentie of atomische verwerking verplicht.
Moet de eindgebruiker direct weten dat de totale actie mislukt is?Kritieke stappen moeten vóór succesrespons voltooid zijn.

Voorbeeld relatie-uitnodiging:

StapKritiek?Reden
RelationshipInvitation aanmakenJaZonder uitnodiging bestaat de actie niet.
Systeembericht aan ontvanger maken wanneer dit de primaire ingang isJaZonder bericht kan de ontvanger de uitnodiging mogelijk niet behandelen.
Badge/teller bijwerkenNeeKan opnieuw worden afgeleid of hersteld.
Technische logging/correlation vastleggenJa, voor zover nodig voor herleidbaarheid van de transactieMoet reconstructie mogelijk maken.

Voorbeeld gedeelde oefening:

StapKritiek?Reden
Shared exercise record aanmakenJaDit is de bron van de gedeelde oefening.
Relatievoorwaarde controlerenJaZonder actieve vriendschap mag de actie niet plaatsvinden.
Systeembericht makenAfhankelijk van functionele ingangKritiek als het bericht de enige ingang is; retrybaar als de gedeelde oefening ook via een eigen overzicht zichtbaar is.
Readmodel/teller bijwerkenNeeAfgeleid en herstelbaar.

De regel is bewust voorzichtig: bij twijfel wordt een stap als kritiek behandeld totdat is onderbouwd dat retrybare naverwerking veilig is.

3.16 Scheduling en jobcontracten

OefenHub.Scheduling is eigenaar van de technische lifecycle van jobs nadat een job succesvol is aangemaakt. Domeinmodules blijven eigenaar van de domeinlogica die door de job wordt uitgevoerd.

De aanmaak van een job loopt via een scheduling-contract. De uitvoering loopt terug naar het betrokken domein via een publiek contract.

Scheduling beheert:

  • jobtype;
  • payloadreferentie of payload;
  • correlation-id;
  • pogingenteller;
  • retryconfiguratie;
  • laatste fout;
  • status;
  • technische jobhistorie;
  • interne beheerbaarheid.

De domeinmodule beheert:

  • businessregels;
  • domeinvalidatie;
  • domeinmutaties;
  • domeintransaction boundary;
  • domeinspecifieke audit/history.

Bij multi-domain jobs moet per jobtype expliciet worden vastgelegd of de uitvoering atomair moet zijn, compensatie vereist, retrybaar is of bij fout in een beheerbare failed-status eindigt.

3.17 Logging en correlation door lagen heen

Alle domeinoverstijgende processen moeten herleidbaar zijn via een correlation-id. Dit geldt voor directe HTTP-requests, cross-module workflows en jobs.

Minimaal relevante context:

VeldGebruik
CorrelationIdKoppelt request, workflow, job en domeinacties.
ActorUserIdGebruiker die de actie startte, indien beschikbaar.
ActorRoleContextRolcontext waarin de actie is gestart, indien beschikbaar.
ModuleNameModule die de actie uitvoert.
OperationNameNaam van command, query, workflow of jobstap.
JobIdAlleen bij geplande of retrybare jobs.
AttemptNumberAlleen bij retrybare verwerking.
ObjectType en ObjectIdFunctionele verwijzing naar geraakt object, zonder gevoelige payloads.

Deze loggingafspraak vervangt geen domeinspecifieke historytabellen. Domeinhistory blijft bij het verantwoordelijke domein. Correlation logging zorgt voor technische reconstructie over modules heen.

3.18 Web-projectstructuur op hoofdlijn

OefenHub.Web krijgt een eigen structuur omdat UI, Blazor/Razor-componenten, routing, state en page composition andere ordeningsregels hebben dan domeinmodules.

OefenHub.Web/
Components/
Shared/
Student/
Teacher/
Guardian/
Admin/
Messages/
Tickets/

Pages/
Public/
Account/
Student/
Teacher/
Guardian/
Admin/
Messages/
Tickets/
Profile/

Layouts/

Navigation/

State/

Forms/

Validation/

ViewModels/

PageComposition/

Extensions/

wwwroot/
MapDoel
ComponentsHerbruikbare UI-componenten, rol- of domeingericht geordend; terugkerende MudBlazor-combinaties worden als OefenHub-wrappercomponenten geïsoleerd.
PagesRoutebare pagina's of page-level Razor/Blazor-bestanden.
LayoutsHoofd-layouts, rolcontextlayouts en shellstructuur.
NavigationMenustructuren, navigatiecompositie en presentatielogica voor navigatie.
StateUI-state, browserstate en tijdelijke componentstate; geen autorisatiebron.
FormsFormuliermodellen en formulieropbouw voor UI.
ValidationUI-validatie en formulierfeedback; domeinvalidatie blijft in modules.
ViewModelsSamengestelde modellen voor pagina's en componenten.
PageCompositionServices die pagina-viewmodels samenstellen via modulequery-services.
ExtensionsWebhost-, middleware- en dependency-injectionregistratie.
wwwrootStatische assets.

Regels voor OefenHub.Web:

  • geen EF Core DbContexts;
  • geen module-interne entities;
  • geen businesslogica;
  • geen autorisatie op basis van alleen clientstate;
  • wel page composition via publieke query-services;
  • wel UI-state voor presentatie, formulieren en tijdelijke interactie;
  • wel middleware/pipelineconfiguratie, waaronder security headers, HSTS, CSP en rate limiting.

3.19 Concrete technische oefenmodules

Concrete technische oefenmodules volgen een aparte projectvorm:

OefenHub.Modules.<ModuleCategory>.<ModuleName>/
Contracts/

Models/
Enums/

Services/
Interfaces/

Generation/

Evaluation/

Validation/

Rendering/

Pdf/

Helpers/

Extensions/

Voorbeelden:

OefenHub.Modules.Arithmetic.AdditionSubtractionSimple/
OefenHub.Modules.Arithmetic.Fractions/
OefenHub.Modules.Language.Spelling/

De technische modulecategorie is onderdeel van de projectnaam. Zij hoeft niet één-op-één gelijk te zijn aan de functionele categorie in de oefencatalogus. Voor de eerste V1.0-module gebruikt Optellen & Aftrekken (simpel) het technische project OefenHub.Modules.Arithmetic.AdditionSubtractionSimple; het moduledossier staat onder Oefenmodules / Rekenen.

MapDoel
ContractsEventuele module-eigen publieke types naast het centrale modulecontract.
ModelsModuleconfiguratie-DTO's, vraagmodellen, antwoordmodellen en interne modellen.
GenerationVraaggeneratie.
EvaluationAntwoordcontrole en scorebepaling binnen modulegrens.
ValidationConfiguratie- en inputvalidatie.
RenderingSchermrepresentatie van vragen, antwoorden of module-specifieke notatie.
PdfModule-specifieke PDF/exportrepresentatie waar nodig.
HelpersKleine module-lokale hulpfuncties.
ExtensionsModule-registratie of dependency injection.

Concrete oefenmodules mogen primair afhankelijk zijn van OefenHub.Modules.Abstractions. Gebruik van OefenHub.SharedKernel is toegestaan wanneer dit duplicatie voorkomt en het type aantoonbaar generiek is. Concrete oefenmodules mogen niet afhankelijk worden van OefenHub.Catalog, OefenHub.Practice, OefenHub.Web of interne implementatie van OefenHub.ExerciseModuleHost.

Concrete oefenmodules hebben standaard geen eigen DbContext. Configuratie en runtimegegevens worden generiek opgeslagen via Catalog en Practice, terwijl de module verantwoordelijk blijft voor configuratievalidatie, vraaggeneratie, antwoordcontrole, rendering en PDF-representatie.

3.20 SharedKernel-gebruik

OefenHub.SharedKernel is beperkt bedoeld. Het project mag niet uitgroeien tot een verzameling restlogica.

Toegestaan:

  • generieke resulttypes;
  • clock/time abstractions;
  • correlation-id types;
  • eenvoudige generieke value objects;
  • marker interfaces wanneer echt projectbreed nodig;
  • generieke exceptions of foutcodes wanneer zij niet domeinspecifiek zijn.

Niet toegestaan:

  • domeinservices;
  • module-specifieke DTO's;
  • repositories;
  • business rules;
  • enumwaarden die bij één module horen;
  • UI-modellen;
  • EF Core entities.

Wanneer twijfel bestaat, blijft het type in de module totdat meerdere modules aantoonbaar dezelfde generieke bouwsteen nodig hebben.

3.21 Infrastructure-gebruik

OefenHub.Infrastructure bevat technische integratie die niet bij één functionele module hoort. Het project mag geen functionele domeinregels bevatten.

Voorbeelden van toegestane inhoud:

  • configuratiebinding voor technische opties;
  • adapters voor externe technische voorzieningen;
  • loggingintegratie;
  • generieke host-extensies;
  • infrastructuurabstractions die niet domeinspecifiek zijn;
  • technische helpers voor environment/configuratie.

Securityconfiguratie zoals HSTS, CSP, rate limiting en security headers wordt in het Technisch Ontwerp volledig beschreven, maar krijgt in de eerste baseline geen apart OefenHub.Security project. De registratie zit primair in OefenHub.Web en waar nodig in OefenHub.Infrastructure of extension methods.

3.22 Dependency injection en module-registratie

Elke module biedt een extension method voor registratie van de eigen services en, indien van toepassing, DbContext.

OefenHub.Practice/
Extensions/
ServiceCollectionExtensions.cs

Voorbeeld:

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPracticeModule(
this IServiceCollection services,
IConfiguration configuration)
{
// registreer modulecontracts, interne services en PracticeDbContext
return services;
}
}

OefenHub.Web roept deze registraties aan tijdens applicatiestart. De Web-host hoeft de interne classes niet te kennen; de module registreert zelf de implementaties achter haar publieke contracts.

3.23 Validatie en foutafhandeling per laag

LaagValidatie/foutafhandeling
UI/WebFormaatvalidatie, verplichte velden, gebruikersfeedback en aanroepen van modulecontracts. Geen definitieve autorisatie- of domeinbeslissingen.
ModulecontractControle op requestvorm, actorcontext en minimale invoer.
Module-serviceDomeinvalidatie, autorisatiecontrole via relevante contracten, statusovergangen en transaction boundary.
Data-laagConstraints, unique indexes, concurrency en persistencefouten.
SchedulingJobstatus, retrybeleid, pogingenteller, technische foutregistratie en failed-status.
InfrastructureTechnische fouten rond configuratie, externe adapters en hostintegratie.

Gebruikersgerichte foutmeldingen verwijzen naar bestaande popup-/feedbackmechanismen waar die functioneel zijn vastgelegd. Het Technisch Ontwerp beschrijft technische foutafhandeling, logging en propagation, maar introduceert geen nieuwe functionele foutteksten.

3.24 Testprojectstructuur

Testprojecten staan onder de solution folder of fysieke map tests.

tests/
OefenHub.Practice.Tests/
OefenHub.Relationships.Tests/
OefenHub.Support.Tests/
OefenHub.Modules.Arithmetic.Addition.Tests/
OefenHub.ArchitectureTests/
OefenHub.IntegrationTests/

Binnen een testproject mogen tests naar behoefte worden geordend:

OefenHub.Practice.Tests/
Unit/
Integration/
Contracts/
TestData/

De exacte testindeling hoeft niet voor elk project identiek te zijn. Per module wordt gekozen welke testvormen nodig zijn om de lading te dekken.

Architecture tests bewaken aanvullend:

  • modules referencen OefenHub.Web niet;
  • module-interne namespaces worden niet buiten de module gebruikt;
  • contracts lekken geen EF Core entities;
  • concrete oefenmodules referencen geen Catalog, Practice of Web;
  • SharedKernel bevat geen module-specifieke types.

3.25 Niet toegestane patronen

Niet toegestaanReden
Webcomponent gebruikt direct een module-DbContext.Doorbreekt modulegrens en autorisatie-/serviceafspraken.
Module A gebruikt entity uit ModuleB.Data.Entities.Maakt modules compile-time en persistence-technisch gekoppeld.
Cross-module query via directe SQL op andermans schema zonder contract.Omzeilt eigenaarschap en maakt refactoring risicovol.
Publieke DTechnisch Ontwerp bevat EF Core entity.Lekt persistence-implementatie naar andere modules.
SharedKernel gebruiken voor domeinspecifieke enums.Maakt SharedKernel een vuilnisbak en verzwakt eigenaarschap.
Concrete oefenmodule refereert Practice of Catalog.Oefenmodulecontract wordt dan afhankelijk van platformimplementatie.
Scheduling voert businesslogica zelf uit.Scheduling is eigenaar van joblifecycle, niet van domeinregels.

3.26 Verwijzing naar detailhoofdstukken

Dit hoofdstuk legt de algemene structuur en dependency-richting vast. Belangrijke detailhoofdstukken zijn: