Technologie
Entity Framework
Code First-approach
In dit blog nemen we jullie graag mee in hoe wij Entity Framework toepassen in onze development projecten.
Bij bijna elk project zetten we standaard een basis Entity Framework (EF) op, dit doen we door middel van de ‘Code-First-approach’. Hiermee zetten we de structuur van de database op in code, waarna je via automatisch gegenereerde migrations de database bijwerkt om deze overeen te laten komen met de code.
Dit zorgt ervoor dat je update-queries op de database kunt uitvoeren via C#-code. Hierdoor hoeven onze startende developers maar weinig te leren over SQL, uiteraard wordt het werk gecontroleerd in pull requests.
Waar dit nog meer in terugkomt is dat aanpassingen voor de database beginnen in code en dus in onze source control. De database van een omgeving is daardoor altijd bij met de code die de database omschrijft.
Je krijgt daardoor ook op je entiteiten duidelijk te zien wat de verplichtingen en verwachtingen zijn dankzij de code based annotations, Zonder dat dit een trip naar de database vereist:
Entiteiten zijn voor dataopslag
Doordat de database nu in code beheerd wordt, krijg je al gauw de neiging om ook de entiteiten te gebruiken als modellen binnen de applicatie logica. Klinkt vrij logisch: waarom zou je bijvoorbeeld een “CreateModel” maken als de velden die je nodig gaat hebben, al op de database model beschikbaar zijn? Dit is een bad practice, bovenal omdat je dan de controle van je entiteiten vrijgeeft door de hele applicatie. Niet langer is je entiteit een model wat de datastructuur weergeeft, het is dan ineens ook een model wat de data vasthoudt voor gebruik in de applicatie, of zelfs wat buiten de repository laag aangemaakt kan worden. Aanmaken buiten de repository laag is bovenal een slechte zaak omdat je afgaat van een centrale plek waar dit gebeurt en dit correct beheren moeilijker is. Dus wat voor hulpmiddelen zijn er om te voorkomen dat dit gebeurt?
Duidelijke naamgeving is essentieel
Daarom is het essentieel te zorgen voor een goede naamgeving, als voorbeeld deel ik de benamingen uit een klant uit de scheepsvaartindustrie. Daar maken we gebruik van drie entiteiten: Voyage, Vessel en Captain. Deze krijgen daarom de volgende benamingen: VoyageEntity, VesselEntity en CaptainEntity toebehorend aan de tabellen: Voyages, Vessels en Captains. Uiteraard hebben ook de datasets dezelfde naam als de tabellen:
Door de Entity-toevoeging is het makkelijk te herkennen waar je aan het werk bent, helemaal omdat Entity een toevoeging is die alleen voor de entiteiten gereserveerd is.
Op zichzelf staande database
Nu we een redelijk duidelijke en beheersbare database hebben is het ook zaak om deze goed te gebruiken. Daarvoor gebruiken we de volgende basisregels:
· De entiteit modellen zijn bedoeld voor data opslag, ze worden niet gebruikt buiten de repositories/datacontext. Dit betekent dus ook dat ze geen input/output voor functies mogen zijn.
· Repositories zijn hetzelfde als de standaard services behalve dat ze toegang hebben tot de datacontext en dus mogen werken met de entiteiten.
Door beperkingen te leggen op de bereikbaarheid van entiteiten ben je verplicht beter na te denken over wat je uit de database ophaalt. Het via de Include operator ophalen van allerlei gekoppelde tabellen (wat extreem slecht voor performance is), is niet langer mogelijk. Doordat je vaak ook nieuwe modellen moet maken ben je ook eerder geneigd om de variabelen een betere naam te geven, niet langer haal je de gehele Voyage met zijn gehele Capitain-entiteit op maar zorg je nu voor een VoyageOverviewModel met daarop CaptainName als variabele.
Deze gewenste limitatie komt nog duidelijker terug bij het aanmaken of bijwerken van entiteiten. Waar dit in de meest vrije vorm gedaan kan worden op ieder punt in de applicatie, dient het nu centraal te gebeuren, waarbij alle velden welke nodig zijn voor een minimale entiteit op hetzelfde moment gespecificeerd worden. De verantwoordelijkheid voor het opvullen van standaard-geformatteerde velden ligt nu op een centrale plek.
Als een concreet voorbeeld: bij het aanmaken van een Captain zal de Callsign gegeneerd worden aan de hand van de naam en het ID. Hierdoor zullen zowel het CreateModel als UpdateModel deze variabele niet bevatten. Maar deze kan dus wel in de repository gezet worden en dus door het beperken van het entiteit gebruik is er ook geen mogelijkheid om dit veld op een andere manier te zetten.
Testing
Een groot voordeel van de EF ‘Code-First approach’ is ook hoe makkelijk deze te testen zijn nadat je dit eenmaal hebt opgezet. Gezien de basis code er al is kun je gemakkelijk testen draaien, al voordat er aanpassingen zijn doorgevoerd. EF maakt dit nog makkelijker door allerlei libraries, zoals MOQ.
Zelf ben ik een groot fan van het gebruik van een simpele localDb, deze kan doormiddel van local SQL files (.mdf) de code draaien op bijna dezelfde manier als de daadwerkelijk productie SQL database/Server in Azure. Er zijn ook opties om dit te doen met een InMemory database. Deze gedraagt zich echter niet in alle gevallen hetzelfde als een daadwerkelijke database. Dit kan dus zorgen voor een vreemd, onverwacht testresultaat welke niet in productiescenario’s optreedt. We willen uiteraard zo dicht mogelijk bij de realiteit van productie zitten.
Met een simpele DbContext helper, en een net zo'n simpel gebruik, heb je binnen de kortste keren je eigen repository testen.
Kanttekening
Wel moet ik zeggen dat in deze manier van setup, de testen in de pipeline gedraaid worden. Hierdoor worden de testen dan wel erg traag. Met 500 repository testen zit ik momenteel al op 20 minuten, wat een groot nadeel is aan deze werkwijze.
Desondanks ben ik zelf echt voorstander van deze testen gebruiken voor de cruciale queries, of functies van je repositories. Als je deze gebruikt om alles te testen, denk dan na over de performance, of misschien het niet draaien van de gehele set in de pipeline.
Testing met builders
Om door te bouwen op onze testing, het testen van een repository gaat nog makkelijker als je werkt met builders.
Builders zijn extensies op entiteiten, gelokaliseerd in het testproject, die beheren hoe de entiteiten gemaakt worden voor testen. Hierdoor hoeft niet elke test individueel te weten welke velden verplicht zijn voor het aanmaken van een entiteit, of welke combinatie van waardes een bepaalde status reflecteert.
Als voorbeeld pak ik weer de CaptainEntity met een kleine uitbreiding:
Zoals je ziet kan dit een best duidelijk leesbare test geven. Denk je eens in dat je bovenstaande functie moest maken/beheren zonder de builder. Deze test hoort niet te weten wat ‘retired’ precies is, deze test wil alleen kunnen zeggen dat bepaalde captains retired zijn, en andere niet.
Om het helemaal bij concepten te houden, kun je bijvoorbeeld nog een functie maken die heet: AsActiveCaptain om te zorgen dat je test werkt los van wat wij zien als een Default. Die maak ik meestal pas aan als ik opmerk dat dit van toegevoegde waarde is, of de Default is onduidelijk in zijn status (bijvoorbeeld een reis die in veel verschillende statussen kan zijn).
Zoals je ziet kan dit een best duidelijk leesbare test geven. Denk je eens in dat je bovenstaande functie moest maken/beheren zonder de builder. Deze test hoort niet te weten wat ‘retired’ precies is, deze test wil alleen kunnen zeggen dat bepaalde captains retired zijn, en andere niet.
Om het helemaal bij concepten te houden, kun je bijvoorbeeld nog een functie maken die heet: AsActiveCaptain om te zorgen dat je test werkt los van wat wij zien als een Default. Die maak ik meestal pas aan als ik opmerk dat dit van toegevoegde waarde is, of de Default is onduidelijk in zijn status (bijvoorbeeld een reis die in veel verschillende statussen kan zijn).
Dit word nog duidelijker, als je werkt met de complexere entiteiten:
Met de volgende test:
Door het versimpelen van het aanmaken van de entiteit, hoeft de developer zich helemaal niet druk te maken over wat een Default allemaal nodig heeft. Je besteedt het aanmaken van de entiteit gewoon uit, en zorgt dat AsDefault altijd een minimale entiteit aanmaakt die opgeslagen kan worden in de EF. AsDefault is in mijn ogen ook niet de standaard zoals we het in de acceptatie verwachten. Het is de minimale standaard die onze EF in zijn dataset verwacht. Daarom zet ik ook geen status in AsDefault; het is geen verplichting, terwijl Vessel en Captain dit wel zijn voor een Voyage.
Omdat er geen AsStandard is, zorgt dit er ook voor dat je maar zelden AsDefault gebruikt voor zijn waardes, je schrijft altijd een extension om de entiteit te krijgen zoals jij hem in je test nodig hebt. Maar daarnaast hoeft de developer zich dus wel nooit druk te maken om al de overige Entity velden.
Krijgt onze Voyage nu bijvoorbeeld een verplichte entiteit erbij, zoiets als: BillingParty, dan hoeft deze alleen toegevoegd te worden aan de AsDefault en alle andere repositories en testen zouden het nog steeds blijven doen. Dat was nu precies de bedoeling!
Over de schrijver
Ik ben Robbert en ik werk als NET developer bij Garansys. Zelf heb ik een grote voorliefde voor testing en al helemaal voor EF omdat die het zo simpel maakt. Het leek mij leuk om met jullie te delen hoe wij dit aanpakken. Zelf leer ik ook nog continu wat goed werkt en wat niet. Heb jij tips, tricks of feedback voor mij? Ik hoor het graag!
Meer weten? Neem contact op.
Patrick Severijns
Business Unit Manager
06-51150885
p.severijns@garansys.nl