Centric connect.engage.succeed

Het dynamic keyword in C#

Geschreven door Redactie Craft - 26 oktober 2017

Redactie Craft
In C# 4.0 werd het dynamic keyword geïntroduceerd. Een aantal jaar na deze introductie ging ik ermee aan de slag. Mijn doel was om dynamische XML-bestanden uit te lezen alsof het JSON was. Maar helaas, het krachtige dynamic bleek uiteindelijk te ingewikkelde code om op te leveren. Toch maakte dat de zoektocht niet minder interessant.

Onlangs liep ik bij een klant tegen een situatie aan waarin ik XML moest uitlezen in C#. Op zich niet zo heel bijzonder natuurlijk, ware het niet dat deze XML in een van zijn veldjes ook weer XML bevatte. En deze geneste XML was dynamisch; er was geen vooraf gedefinieerd schema. Hieronder zie je in field3 de dynamische, geneste XML.

Figuur 1: XML met geneste XML
Figuur 1: XML met geneste XML

Het idee van het uitlezen van dynamische data deed me denken aan het gemak waarmee dit zou kunnen als het geen XML in C# maar JSON in JavaScript was geweest. Dit zou op onderstaande manier gaan:

JSON uitlezen in JavaScript
Figuur 2: JSON uitlezen in JavaScript

Wat mij hierin zo aanspreekt, is het gemak van het uitlezen. Zoals je ziet op regel veertien is het aan elkaar ‘dotten’ van de JSON-objecten, om zo uiteindelijk een veldje uit te lezen, heel simpel. Dit was eigenlijk hoe ik mijn dynamische XML zou willen uitlezen, maar in C# kun je niet zomaar iets ‘aan elkaar dotten’. Tenzij…

Terug in de tijd

We gaan even terug in de tijd naar de DevDays 2010 in Den Haag. Op het podium vertelt Anders Hejlsberg (bekend als de maker van C#) over een nieuwe feature in C# 4.0: het dynamic keyword!

Met het dynamic keyword kun je tegen objecten aan programmeren, zonder dat de compiler weet van welk type deze objecten zijn. De compiler negeert deze objecten en pas bij het uitvoeren van de code wordt geprobeerd of dat überhaupt wel kan. De volgende voorbeelden geven je hiervan een indruk:

trongly typed, impliciete cast naar object, en het dynamic keyword
Figuur 3: Strongly typed, impliciete cast naar object, en het dynamic keyword

Het eerste voorbeeld is een List van strings die in een variabele van het type List<string> wordt opgeslagen. De klassieke manier dus. In die List kunnen we door middel van de Add-methode een string toevoegen.

In het tweede voorbeeld wordt de List van strings in een object opgeslagen. Dat kan, want alles is een object, dus List<T> ook. Maar omdat de variabele nu een object is en geen List<string> kun je de Add-methode niet aanroepen. De compiler zal op regel zeven een error geven. Het gaat pas goed als je het object expliciet terug naar een List<string> cast.

In het laatste geval wordt het dynamic keyword gebruikt. Hierdoor wordt pas bij het uitvoeren van het programma gekeken of het object dat in de variabele dynamicExample is opgeslagen wel een Add-methode bevat. Zo ja, dan gaat het goed. Anders wordt er een exception gegooid.

Een dynamic compileert dus altijd. We kunnen dus met een dynamic tijdens het programmeren gewoon de velden aan elkaar dotten. Als ik nu mijn XML in een dynamic zou kunnen krijgen, zou ik dan niet op de JavaScript-manier mijn programma kunnen schrijven? Om dat voor elkaar te krijgen moeten we eerst weten wat een dynamic nou eigenlijk precies is.

Onder water

Achter de schermen van C# bestaat er geen type met de naam dynamic. Het is een keyword om tegen de compiler te zeggen dat die op dit object geen checks hoeft te doen. De compiler beschouwt het object dan verder als het type ‘object’. Ook runtime is er geen dynamic, maar op dat moment kan je object van ieder willekeurig type zijn.

DynamicXmlParser

Even terug naar wat we uiteindelijk willen doen: De dynamische XML uitlezen door het aan elkaar dotten. Hieronder staat de code die ik wil kunnen uitvoeren.

XML uitlezen met DynamicXmlParser
Figuur 4: XML uitlezen met DynamicXmlParser

Om dit te kunnen doen, maken we de class DynamicXmlParser aan. Hierin wrappen we de XML, zodat we die vervolgens dynamisch kunnen uitlezen. De opzet van deze class maken we op de volgende manier:
Opzet van de DynamicXmlParser class
Figuur 5: Opzet van de DynamicXmlParser class

Ten eerste maken we een class die overerft van DynamicObject (te vinden in System.Dynamic). In ons geval willen we iets met XML gaan doen, dus we maken een private variabele om een XElement in op te slaan, en een constructor om deze te kunnen vullen.

Vervolgens moeten we de TryGetMember-methode overriden en implementeren. Deze methode wordt runtime automatisch aangeroepen voor ieder veld dat we van ons dynamic object proberen uit te lezen. In TryGetMember moeten we dus in de XML gaan zoeken naar het veld waarvan we de waarde proberen op te vragen. Hieronder staat de uiteindelijke code. 

Implementatie van TryGetMember
Figuur 6: Implementatie van TryGetMember

Eens zien wat hier allemaal gebeurt. Er komt een object van het type GetMemberBinder binnen, en er gaat een resultaat uit. Dit GetMemberBinder-object bevat de informatie over wat er in de aanroepende code wordt opgevraagd.

Stel dat we de volgende velden aan elkaar dotten: ‘dynamicXml.dynamicObject.property2’. Dan zal de binder in eerste instantie informatie over ‘dynamicObject’ bevatten (het onderdeel na de eerste dot). Zoals je ziet, zoeken we op regel zes naar een XML-attribuut met die naam. Wordt deze niet gevonden, dan zoeken we op regel veertien naar een XML-element met deze naam.

Zijn er meerdere elementen met deze naam, dan wordt er op regel achttien een List van gemaakt. Is er maar één element gevonden, dan wordt dat ene element gekozen.

Tot slot kan het nog zijn dat het element (of de elementen) zelf ook weer XML-elementen bevat. In dat geval wordt een nieuwe DynamicXmlParser, met als root het gevonden XML-element, geretourneerd (en gaat het hele verhaal recursief verder). Anders retourneren we gewoonweg de waarden van de gevonden elementen.

Met ‘return true’ geven we aan dat we klaar zijn met zoeken. Zou je ergens ‘return false’ doen, dan kan het zijn dat er in de aanroepende code een exception gegooid wordt (maar dat is afhankelijk van de implementatie van de aanroepende code). Ik wil zelf een validatie kunnen doen, dus ik return overal true.

Gebruik

De wrapper is klaar en we kunnen hem gebruiken op de manier als in figuur 4. Het is hierbij belangrijk dat we ons dynamicXml-object opslaan in een dynamic (dus geen var of object). Zouden we dit niet doen, dan kunnen we alsnog niet de velden aan elkaar dotten.

Doordat DynamicXmlParser overerft van DynamicObject kan dat en wordt daardoor onder water ook automatisch de TryGetMember-methode uitgevoerd. Dit werkt zoals verwacht en ReadProperty2 geeft uiteindelijk de waarde van property2 terug.

Maar daarmee zijn we er nog niet, alle code moet uiteraard worden gedekt door unit tests. Al is het alleen maar omdat de compiler niet zal klagen over eventuele typo’s.

Unit Testing

Omdat we nu overal met dynamics werken, is het zaak om hier in de tests van de aanroepende code mee om te kunnen gaan. Hiervoor kun je ExpandoObject gebruiken. Dit type lijkt op een Anonymous Type, in de zin dat je er alles in kunt zetten.

In de code hieronder zie je dat in het object fakeInput zomaar velden kunnen worden gevuld, en dat we dat object als een dynamic aan de HandleDynamic-methode kunnen meegeven.
ExpandoObject in een Unit Test
Figuur 7: ExpandoObject in een Unit Test

Op deze manier kun je een methode die een dynamic verwerkt testen. Opvallend is trouwens dat in deze code de variabele ‘result’ ook weer een dynamic is, ondanks het feit dat HandleDynamic volgens zijn signature (op regel één) een DateTime-object teruggeeft. Dit komt doordat het dynamic keyword er hier voor zorgt dat er geen compile-time checks worden gedaan, en dus is alles dan automatisch dynamic.

Voor simpele unit tests werkt bovenstaande nog, maar wil je methoden testen die wat ingewikkeldere businesslogica bevatten, dan wordt je unit test code al vrij snel heel lastig te doorgronden.

Beperkingen

Ik ben uiteindelijk gestopt met dynamic toen het zo goed als onmogelijk bleek om een ToString-methode te implementeren (dat zou onder andere het loggen en debuggen een stuk gemakkelijker maken). In de class DynamicXmlParser kun je zonder problemen de ToString-methode overriden. Je krijgt dan wel problemen in de unit tests met het ExpandoObject, omdat je daar geen ToString aan kunt koppelen.

Je zou weliswaar met een delegate of een Func kunnen werken, maar daar wordt de code ook niet bepaald leesbaarder van.

Dit was voor mij de voornaamste reden om maar te stoppen met het hele dynamic-verhaal. Ik vond de code nu onnodig ingewikkeld worden, omdat er een aantal hele simpele alternatieven voorhanden zijn.

Alternatieven

Er zijn verschillende alternatieven denkbaar voor het probleem dat ik met dynamic heb geprobeerd op te lossen. Ik zou bijvoorbeeld AutoMapper kunnen gebruiken, maar daarvoor is mijn dynamische XML weer net wat te complex.

Uiteindelijk heb ik het op de ouderwetse manier aangepakt: de XML deserialiseren naar gegenereerde classes. Dit is uiteindelijk minder werk en levert code op die veel beter leesbaar is. Bovendien hoef ik nu geen kunstgrepen toe te passen om te kunnen loggen én unit testen.

Conclusie

Het dynamic keyword dat in C# 4.0 werd geïntroduceerd, is bijzonder krachtig en ook heel interessant om eens mee te spelen. Maar vanwege de enorme flexibiliteit voegt het ook al gauw veel complexiteit toe aan je code.

In mijn geval had ik er weinig baat bij, omdat ik met een meer klassieke oplossing uiteindelijk sneller af was. Bedenk dus voordat je dynamic gaat gebruiken of je het ook echt nodig hebt, of dat het op een andere manier misschien beter kan. Dat wetende kan het natuurlijk nooit kwaad om er eens mee te spelen. Het was voor mij in ieder geval een interessante en leerzame ervaring.

Links

Wil je meer weten over Craft, hét groeiprogramma voor IT'ers? Neem een kijkje op de Craft-website!

Tags:.NET

     
Reacties
    Schrijf een reactie
    • Captcha image
    • Verzenden