Code voor code – Reflection in .NET

Dit is een reeds eerder gepubliceerde post van de vorige VBcentral website door Sander Rossel.
Als .NET programmeur is de kans groot dat u vroeg of laat in aanraking gaat komen met Reflection. Reflection is de mogelijkheid om tijdens het runnen van uw code deze code te kunnen 'bekijken' en aan te passen. Denkt u zich bijvoorbeeld eens in dat u wilt loggen in uw applicatie. Nu is niet iedere applicatie geschikt om te loggen naar bijvoorbeeld de Windows Event Viewer, een SQL database of een flat text file. Toch wilt u alle mogelijkheden open houden en in de toekomst zelfs meer loggers toe kunnen voegen zonder uw hele source code aan te moeten passen. Dat klinkt ingewikkeld, en dat is het ook. Gelukkig is dat dan ook niet wat ik in deze blog wil bespreken. Wat ik wel wil bespreken zijn enkele technieken die kunnen helpen om een dergelijk scenario te verwezenlijken. Al deze technieken kunnen onder een gezamelijke noemer geschaard worden. Reflection.

Zo is het mogelijk om een nieuw Object te creëeren zonder het 'New' keyword te gebruiken. Dit zou u bijvoorbeeld willen doen als u het Type van het aan te maken Object nog niet weet (zoals in het geval van de logger, gebruiken we een SqlLogger of toch een FlatTextLogger?). Kijkt u bijvoorbeeld eens naar het volgende stukje code. 

 
' De 'normale' manier om een nieuwe SqlLogger te instantiëren. 
 
Dim l As SqlLogger = New SqlLogger() 
 
' Een SqlLogger aanmaken via Reflection. 
Dim l As SqlLogger = Activator.CreateInstance(Of SqlLogger) 


De naam van de functie zegt eigenlijk al genoeg. We maken een nieuwe instantie van het Type SqlLogger. Uiteraard zal u dit in de meeste gevallen niet nodig hebben omdat u voor deze syntax het Type SqlLogger al moet weten tijdens design-time. Laten we kijken naar nog een voorbeeld om een Logger te creëeren zonder het New keyword te gebruiken. Overigens gaan we er voor de rest van deze blog van uit dat iedere Logger de ILogger Interface implementeert. Op die manier hebben we een set algemene functies (zoals Log) die voor alle Loggers van toepassing is. 

 
Dim l As ILogger = DirectCast(Activator.CreateInstance(GetType(SqlLogger)), ILogger) 


Als u hierboven GetType(SqlLogger) vervangt door een parameter worden de mogelijkheden al uitgebreider! Zo zou een ILogger ook het Type EventLogger of FlatTextLogger kunnen zijn, die van buitenaf wordt meegegeven aan een functie. Overigens is deze methode niet strong-typed. CreateInstance geeft een Object terug dat gecast zal moeten worden naar een ILogger. Als deze functie dus aangeroepen wordt met een Type dat niet ILogger implementeert zal er een Exception gethrowed worden. Hier zal u voor moeten waken.

Er zijn nog meer mogelijkheden. Zo kunt u ook middels een String een nieuwe Logger instantiëren, ideaal als u het type Logger per applicatie uit een config bestandje haalt! 
 
 
Dim l As ILogger = DirectCast(Activator.CreateInstance(Type.GetType("MijnApplicatie.SqlLogger")), ILogger) 


Uiteraard wordt "MijnApplicatie.SqlLogger" (de naam van het Type) vervangen door een parameter. U ziet dat u nu middels een text in een configuratie bestand (of database) voor iedere applicatie (of computer) een andere Logger kunt genereren. 
Overigens lukt deze methode enkel als de Constructors van de Logger Types geen parameters hebben. Verwachten ze die wel dan zal er een Exception ge-throw-ed worden. Het is echter wel mogelijk om een nieuw Object te instantiëren en parameters mee te geven. 

 
Dim connectionString As String = "Data Source=myServer;Initial Catalog=myDB;Integrated Security=True;" 
Dim logPogingen As Integer = 3 
Dim l As ILogger 
 
l = DirectCast(Activator.CreateInstance(GetType(SqlLogger), {connectionString, logPogingen}), ILogger) 


In dit geval wordt een Array met parameters meegegeven aan de functie. De functie zal nu zelf de Constructor aanroepen waarbij een String en een Integer als parameters worden verwacht. Let op dat als u de Constructor aanpast, of als de Constructor niet bestaat u hier geen melding van zal krijgen. De code zal wederom een Exception throw-en!

Het is u misschien opgevallen dat ik vaak de functie GetType gebruik. Als u met Reflection bezig bent is de Type Class uw vriend! Een hele hoop functies in de System.Reflection Namespace hebben een Type of een Array van Types nodig om hun werk te kunnen doen. 
Neem bijvoorbeeld het volgende stukje code. We hebben een Object waar we dynamisch, via de naam, een functie op uit gaan voeren. 

 
Dim obj As Object = New SqlLogger 
Dim method As MethodInfo = obj.GetType.GetMethod("Log") 
 
method.Invoke(obj, {}) 


U ziet hier dat we een SqlLogger toekennen aan een Object. Het Object kent geen functie "Log", maar de SqlLogger kent deze wel. We kunnen dan via het Type van de obj variable (die we kunnen verkrijgen door de GetType functie) toch de Log functie opvragen en aanroepen. Het aanroepen gebeurt via de Invoke method die een Object nodig heeft waar de functie op aangeroepen moet worden en een Array van variabelen die aan de functie meegegeven moeten worden. 
Nu zal in het voorbeeld hierboven weinig gelogd kunnen worden. Waarschijnlijker is het als de Log functie bijvoorbeeld een String als parameter mee krijgt die gelogd gaat worden. 

 
Dim obj As Object = New SqlLogger 
Dim method As MethodInfo = obj.GetType.GetMethod("Log", {GetType(String)}) 
 
method.Invoke(obj, {"Log deze text."}) 


U ziet dat door een Array van Types mee te geven aan de GetMethod functie ik nu ook een parameter van dat type mee kan geven aan de Invoke functie. Uiteraard moet deze functie natuurlijk wel echt bestaan op de SqlLogger. 
Overigens kunt u op dezelfde manier een nieuw Object instantiëren. We gebruiken dan niet GetMethod, maar GetConstructor. 

 
Dim ctorInfo As ConstructorInfo = GetType(SqlLogger).GetConstructor({}) 
Dim l As ILogger = DirectCast(ctorInfo.Invoke({}), ILogger) 


U ziet dat u ook aan de GetConstructor functie een Array van Types mee kan geven. U kunt op deze manier ook Properties opvragen. 

 
Dim l As New SqlLogger 
Dim propInfo As PropertyInfo = l.GetType.GetProperty("ConnectionString") 
Dim method As MethodInfo = propInfo.GetGetMethod 
Dim connString As String = Convert.ToString(method.Invoke(l, {})) 


Er is hier iets vreemds aan de hand. We vragen de Property op via GetProperty. Daarna moeten we nog eens GetGetMethod aanroepen. Dit komt omdat een Property onder de motorkap niks meer is dan een wrapper voor een Get_Function en een Set_Function. Om de waarde van een Property op te vragen of te wijzigen zal u dus de GetGetMethod of de GetSetMethod op de PropertyInfo aan moeten roepen.
Dit is niet het enige wat u kunt doen met Reflection. Kijk bijvoorbeeld eens naar het volgende stukje code. 
 
 
For Each method As MethodInfo In GetType(DateTime).GetMethods 
 
   Console.WriteLine(method.Name) 
 
   For Each param As ParameterInfo In method.GetParameters 
      Console.WriteLine("   " + param.Name + " As " + param.ParameterType.Name) 
   Next 
 
Next 


U kunt dit stukje code in een Console Applicatie plakken en u zult alle methods van het DateTime type zien compleet met parameter namen en Types. U zult een aantal gekke method namen zoals get_Minute tegen komen. Dit is de naam van de Get_Function van de Minute Property. U kunt dit truckje herhalen voor Properties, Constructors en Fields. U kunt zelfs toegang krijgen tot Private fields en hun waarden aanpassen! Dit is echter niet aan te raden aangezien deze fields Private zijn voor een reden. U kunt toegang krijgen tot Private fields, methods, properties enzovoorts via een overload van bovengenoemde functies die een BindingFlags als parameter mee krijgen. BindingFlags is een Enumeratie die het soort functie aan geeft wat u wilt hebben. 
In het voorbeeld hierboven gebruik bijvoorbeeld eens de volgende overload. 
 
 
GetType.GetMethods(BindingFlags.Instance Or BindingFlags.NonPublic) 


Met deze BindingFlags worden alle methods teruggegeven die Private en niet Shared zijn. De BindingFlags worden, net als Arrays van Types, veel gebruikt in de System.Reflection Namespace.
Reflection maakt het onder andere mogelijk om een 'plug-in' structuur voor uw applicatie te ontwerpen. Denkt u zich bijvoorbeeld eens in dat u via plug-ins verschillende Loggers voor uw applicatie kunt inladen. Afhankelijk van de dll's die u in de folder C:\MijnApplicatie\Loggers plaatst wordt er gelogd naar verschillende traces. De volgende code zou een dergelijke situatie kunnen verwezenlijken. 
 
 
' Loop door de dll's in C:\MijnApplicatie\Loggers. 
For Each path As String In Io.Directory.GetFiles("C:\MijnApplicatie\Loggers", "*.dll") 
 
   ' Laadt de Assembly in het geheugen. 
   Dim assm As Assembly = Assembly.LoadFrom(path) 
 
   ' Loop door alle Types in de Assembly die ILogger implementeren. 
   For Each t As Type In assm.GetTypes.Where(Function(c) c.GetInterfaces.Contains(GetType(ILogger))) 
      ' Maak een instantie aan van het gevonden Type. 
      Dim l As ILogger = DirectCast(Activator.CreateInstance(t), ILogger) 
 
      ' Doe iets met de logger. 
 
   Next 
 
Next 


Dat was verassend simpel. U kunt nu voor iedere Logger een aparte dll maken en deze desgewenst in de Loggers folder zetten of weghalen. Overigens zou u de Loggers inlezen bij het starten van de applicatie en deze dan in het geheugen vasthouden. Het is niet verstandig om deze loop uit te voeren iedere keer dat er gelogd moet worden. Reflection is (relatief gezien) geen snelle techniek.

Nog een toepassing voor Reflection is het werken met Attributes. Attributes decoreren Classes, Methods, Properties en andere .NET typen. Een Attribute is een Class die Inherit van de Attribute Class en kan als volgt gebruikt worden. 
 
 
Public Class LogStartupAttribute 
 
   Inherits Attribute 
 
   Public Sub Log(ByVal t As Type) 
 
      ' Log dat Type t is opgestart. 
 
   End Sub 
 
End Class 
 
 
Public Class Test 
 
End Class 


Een ander stukje code zou nu het volgende kunnen doen. 

 
Dim t As New Test 
 
For Each attr As LogStartupAttribute In t.GetType.GetCustomAttributes(GetType(LogStartupAttribute), False) 
 
   attr.Log(t.GetType) 
 
Next 


Op deze manier kunt u makkelijk functionaliteit toepassen op bepaalde Types, Properties enzovoorts. U kunt bijvoorbeeld ook de waarde opvragen van een Property en deze ophalen om deze vervolgens aan de Attribute mee te geven die de waarde dan weer kan valideren. Het voordeel is dat Attributes heel makkelijk hergebruikt kunnen worden. Het is voldoende om ze simpelweg boven een andere Class, Property enzovoorts te plakken. U moet er natuurlijk wel voor zorgen dat de Attribute ook aangeroepen wordt door een andere stukje code. Hoe dan ook, de mogelijkheden zijn legio. Overigens kunt u zien dat 'Attribute' wordt weggelaten wanneer deze als Attribute boven Class Test wordt geplaatst. Dit doet Microsoft voor ons. Het is zonder die 'Attribute' toevoeging ook al duidelijk dat we met een Attribute te maken hebben. Toch is het regel om namen van Attributes altijd met Attribute te eindigen.
Het is met Reflection ook mogelijk om runtime code te genereren, compileren en uit te voeren. De makkelijkste methode is om een String direct om te zetten in code en deze uit te voeren. 

 
' Eerst genereren we de code die uitgevoerd moet worden. 
 
Dim s As String = "(5 + 5) * 4" 
Dim code As New Text.StringBuilder 
 
code.AppendLine("Class MyDynamicClass") 
code.AppendLine("Public Shared Function DoSomeMath() As Integer") 
code.AppendLine("Return " + s) 
code.AppendLine("End Function") 
code.AppendLine("End Class") 
 
' We vergkrijgen een reference naar een CodeProvider. 
Dim compiler As CodeDom.Compiler.CodeDomProvider = CodeDom.Compiler.CodeDomProvider.CreateProvider("VB") 
 
' We compileren de code. 
Dim compilerParams As New CodeDom.Compiler.CompilerParameters 
Dim compResult As CodeDom.Compiler.CompilerResults = compiler.CompileAssemblyFromSource(compilerParams, {code.ToString}) 
 
' Vraag het zojuist gegenereerde Type op via Assembly.GetType. 
Dim dynamicType As Type = compResult.CompiledAssembly.GetType("MyDynamicClass") 
 
' Run de DoSomeMath function en converteer het resultaat naar een String. 
Dim result As Integer = Convert.ToInt32(dynamicType.GetMethod("DoSomeMath").Invoke(Nothing, Nothing)) 


U ziet dat het vrij makkelijk kan. Overigens is dit niet de snelste methode om code te genereren en allicht ook niet de handigste (over het algemeen wordt zulke String concatenation een zooitje). Het .NET Framework biedt enkele alternatieven aan om runtime uw eigen Assemblies, Modules en Types te genereren. Dit kan onder andere via de System.Reflection.Emit Namespace. Via deze Namespace kunt u zelf uw eigen Microsoft Intermediate Language (of MSIL) emitten. Dit is wat de compiler normaal gesproken voor u doet als u uw project build. Dit onderwerp is echter flink uitgebreid en redelijk gevorderd. Om die redenen zal ik hier dus ook verder niet op in gaan. Een andere, allicht makkelijker, manier om code te genereren is via de System.CodeDom Namespace. Deze Namespace is uitgebreid en biedt de nodige Classes om runtime code te genereren. Nog een optie om runtime code te genereren is via de System.Linq.Expressions Namespace. Expressions haken in op Reflection.Emit in die zin dat ook Expressions MSIL kunnen emitten. Ook deze Namespace is zeer uitgebreid en ik zal ook hier verder niet op in gaan.

U ziet dat Reflection een scala aan mogelijkheden biedt die nieuwe deuren voor u opent. U heeft slechts het topje van de ijsberg gezien. Toch moet u voorzichtig zijn wanneer u met Reflection werkt. Reflection is een 'broze' techniek in die zin dat als u ergens de naam van een functie of het type van een parameter wijzigt uw code op onverwachte plaatsen kan breken. Visual Studio kan u hier niet van tevoren voor waarschuwen, dus u zal dit pas runtime merken. Daarnaast is Reflection over het algemeen een relatief trage techniek. Ik raad u dan ook aan om Reflection niet roekeloos toe te passen. Pas het toe waar het echt waarde toe kan voegen.

Nieuwsbrief

Blijf op de hoogte van alles wat op VBcentral gebeurd. Meld je nu aan voor onze nieuwsbrief! »

Over ons

Wij zijn gek op het Microsoft .NET ontwikkelplatform en haar ontwikkeltalen, maar we hebben een sterke voorkeur voor Visual Basic .NET! »

Neem contact op

VBcentral.nl
Bataafseweg 20
7101 PA Winterswijk
Nederland
+31 (543) 538 388
info@vbcentral.nl