Reflection als WCF inpakpapier
Sander van de Velde heeft een oplossing bedacht voor het runtime inpakken van een WCF interface waarbij de ingepakte server een andere interface definitie dan de WCF interface ondersteunt. De twee interfaces moeten alleen dezelfde methodes bezitten. Sander maakt hierbij gebruik van het wrapper design pattern en Microsoft Intermediate Language generatie. Dit biedt de mogelijkheid om zelfs een Service locator te bouwen waarbij verschillende services dezelfde interface ondersteunen.
Interface en methode contracten
Tijdens een recentelijk project is een service eerst in Windows Communication Foundation (WCF) en later met Remoting geïmplementeerd. De webserver buiten de firewall (demilitarized zone, DMZ) moest hierbij als cliënt een applicatieserver binnen de firewall aanroepen. De communicatie was in eerste instantie in WCF ontwikkeld. Een WCF service is zo gebouwd, compleet met de WCF attributen ServiceContract en OperationContract op de bijbehorende methodes van de interface beschrijving. Helaas bleek in een later stadium dat op de webserver hoogstens het .Net 2.0 framework beschikbaar was dus moesten we voor de communicatie uitwijken naar .Net Remoting.
Met .Net Remoting was dezelfde oplossing met dezelfde methodes eigenlijk net zo eenvoudig gebouwd maar er waren wel enkele duidelijke verschillen met WCF welke direct opvielen. Ten eerste werd een design time gegeneerde cliënt proxy opgedrongen waar Remoting volstaat met enkele regels code om een cliënt proxy te genereren. Bij iedere wijziging van het servicecontract moest de WCF cliënt proxy opnieuw bijgewerkt worden. Gelukkig bleek deze cliënt proxy toch ook te genereren. Door het gebruik van een ChannelFactory kan dezelfde functionaliteit geboden worden (zie listing 1).
|
[ServiceContract] public interface IWcfService { [OperationContract] string DoWork(); } … ChannelFactory<IWcfService> factory = new ChannelFactory<IWcfService>(new WSHttpBinding()))
IWcfService service = factory.CreateChannel(new EndpointAddress( "http://localhost/WcfHost/Service.svc")); string antwoord = service.DoWork(); ((ICommunicationObject)service).Close(); factory.Close(); |
Listing 1: Een echte WCF aanroep zonder vooraf gegenereerde proxy
Bij WCF maken de cliënt proxy en de server gebruik van dezelfde service interface compleet met de WCF attributen. Hierdoor viel het op dat op de cliënt, de aanroepende partij, het nodig is om een referentie naar System.ServiceModel te leggen. Nu lijkt dit op zich triviaal; deze assembly is namelijk gewoon geïnstalleerd met het .Net framework en dus altijd overal aanwezig. Maar het is wel vreemd dat de gebruikte manier van communiceren zichtbaar wordt voor de aanroepende partij en dat dus afgedwongen wordt dat bepaalde gerefereerde assemblies geladen moeten worden. De WCF interface definitie is dus opdringerig, een cliënt moet niets merken over de manier van communiceren.
De WCF interface definitie is dus opdringerig, een cliënt moet niets merken over de manier van communiceren
Factory design pattern
Dit kan opgelost worden door gebruik te maken van een factory design pattern voor de communicatie. Design patterns zijn standaard oplossingen voor standaard problemen (zoals hier de wens tot abstractie van de communicatie) en binnen Atos Origin proberen wij altijd eerst terug te grijpen op deze bewezen oplossingen.

Figuur 1: Class diagram van factory pattern. De twee interface definities zijn helaas niet gelijk
Bij een factory design pattern wordt een instantie van de interface opgevraagd om bepaalde werkzaamheden uit te voeren, zonder kennis te nemen van de daadwerkelijke implementatie. De beslissing welke klasse wordt geïnstancieerd zal door de factory gemaakt worden. De factory kan bijvoorbeeld afhankelijk van het aanwezige .Net platform de keuze tussen .Net Remoting of WCF maken (zie figuur 1). Maar omdat de twee genoemde technieken ieder een eigen interface definitie vereisen, met of zonder WCF attributen, moet hier een tweede design pattern toegepast worden.
Wrapper design pattern
Het is dus niet mogelijk om WCF zonder de benodigde WCF attributen te laten communiceren. Een WCF cliënt proxy moet de benodigde kennis over de WCF interface bezitten en de WCF cliënt proxy aanroep moet uiteindelijk ergens uitgevoerd worden. Daarom is onderzocht of het mogelijk is om de WCF cliënt proxy wel te blijven aanroepen, maar deze cliënt proxy ‘in te pakken’ met een andere interface. De oplossing voor dit probleem kan uitgewerkt worden in het wrapper design pattern (ook wel adapter genoemd).
Het wrapper design pattern wordt vaak toegepast, het is een fraaie manier om klassen samen te laten werken welke anders niet goed op elkaar aansluiten. Een nieuwe interface wordt letterlijk als inpakpapier rond de aan te roepen logica gelegd (zie figuur 2)

Figuur 2: Class diagram van wrapper design pattern
De wrapper class ondersteunt geheel of gedeeltelijk de logica van de wrapped class maar laat zich aanroepen met zijn eigen wrapper interface. De IWrapperInterface heeft hierbij geen enkele relatie met IWrappedInterface. IWrapperInterface hoeft dus niet eens alle members van IWrappedInterface te ondersteunen. Het is dus ook mogelijk om bepaalde complexiteit van het ingepakte object met het aanroepen van de wrapper te vereenvoudigen. Maar in onderstaand uitgeschreven voorbeeld houden we de twee interfaces gelijk (zie listing 2).
|
public class WrapperClass : IWrapperInterface { private IWrappedInterface _WrappedClass;
public WrapperClass(IWrappedInterface wrappedClass) { _WrappedClass = wrappedClass; }
public string MethodOne(string parameter) { return _WrappedClass.MethodOne(parameter); }
public int MethodTwo(int parameter) { return _WrappedClass.MethodTwo(parameter); } } |
Listing 2: een wrapper in code geschreven
Met een wrapper kan dus nog meer gedaan worden dan alleen maar het doorlussen van de methodes. Zo kan bijvoorbeeld aan iedere aanroep logging, een timer of extreme foutafhandeling toegevoegd worden.
Maar inpakken zoals in het bovenstaande voorbeeld wordt al snel monnikenwerk vanwege het uittypen, vooral indien de interface tijdens de ontwikkeling aan veel wijzigingen onderhevig is.
Het gebruik van reflection voor dynamische code generatie biedt uitkomst om wrappers dynamisch te genereren.
Het gebruik van reflection is een uitkomst bij dynamische code generatie
Bij dit artikel zijn twee implementaties van het wrapper design pattern meegeleverd: een versie via reflection en een versie via overerving van de RealProxy. Beiden zullen behandeld worden.
Reflection en MSIL generatie of RealProxy
Reflection biedt de mogelijkheid om nieuwe types, compleet met methodes en logica, runtime in het geheugen te brengen als Microsoft intermediate language (MSIL). Dit is de ultieme just-in-time (JIT) code generatie maar in de praktijk zie je het maar weinig zichtbaar toegepast worden. Dit komt enerzijds omdat de veilige wereld van code editor en design-time compiler verlaten moet worden. Anderzijds is de leercurve hoog want het komt dicht bij het zelf op de stack zetten van attributen en het aanroepen van methodes om de stack uit te lezen en weer verder te vullen. (kijk voor meer details op http://msdn2.microsoft.com/en-US/library/8ffc3x75(VS.80).aspx)
Wie ervaring heeft met assembler, beleeft hier het feest der herkenning. Toch is er een fundamenteel verschil met assembler. Ten eerste is er tegenwoordig intelliSence en dat scheelt heel wat zoekwerk daar waar vaak een tekst editor het enige gereedschap was. Maar belangrijker is dat deze MSIL wel degelijk typesafe is en blijft! De MSIL generator zal invalide opdrachten bij het samenstellen van het type gewoon afkeuren.
MSIL generatie is enkele factoren trager tov. het gebruik maken van voorgecompileerde code maar gelukkig kan de runtime gegenereerde code opgeslagen worden voor later hergebruik. Wrappen met MSIL heeft de voorkeur in een situatie met een hoge serverload.
Wrappen met MSIL heeft de voorkeur in een situatie met een hoge serverload
In System.Runtime.Remoting.Proxies wordt de abstracte base class RealProxy aangeboden, en deze geeft ook de mogelijkheid om twee interfaces op elkaar te mappen. Alle logica rond het inpakken wordt bij de RealProxy in één invoke methode runtime uitgevoerd, ieder methode aanroep komt hier langs.
Alle logica rond het inpakken wordt bij de RealProxy in één invoke methode runtime uitgevoerd, ieder methode aanroep komt hier langs
Hierdoor is het echt geen vereiste meer dat de twee interfaces gelijkvormig zijn. Voordeel van de RealProxy is dat de C# broncode eenvoudig uit te breiden is met extra logica. Wel zal de RealProxy altijd via reflection de mapping moeten uitvoeren en dat is relatief trager.
Runtime wrapper generatie met MSIL
Onze MSIL wrapper gaat dus runtime twee willekeurige maar ‘gelijkvormige’ interfaces op elkaar laten aansluiten. Hiervoor is een static helper class geschreven om het gewenste type te genereren (zie listing 3).
|
// Step 1: Generate the wrapper class type TypeBuilder typeBuilder = GenerateWrapperType(typeOfWrapperInterface);
// Remember the getter and setter MethodBuilder methodBuilderGet; MethodBuilder methodBuilderSet;
// Step 2: Generate the wrapped object property GenerateWrappedObjectProperty( typeBuilder, typeOfWrappedInterface, out methodBuilderGet, out methodBuilderSet);
// Step3: Generate the constructor GenerateConstructor( typeBuilder, typeOfWrappedInterface, methodBuilderSet);
// Step 4: Generate all methods GenerateWrappedMethodes( typeBuilder, typeOfWrappedInterface, methodBuilderGet);
// Finally, build this wrapper type and return it return typeBuilder.CreateType(); |
Listing 3: Een wrapper helper class in slechts vier stappen
De eerste stap is van administratieve aard. Types kunnen niet gegenereerd worden zonder dat er een assembly voor gedefinieerd is. Ook moet een module aanwezig zijn. Dat is geen namespace maar geeft de mogelijkheid om types te groeperen. Geef hierbij aan de typebuilder door dat een class aangemaakt moet worden en geef ook de overerving op. We gaan een overerving van de Object class met de IWrapperInterface definitie implementeren.
De tweede stap stelt een field en een property samen voor het onthouden van de IWrappedInterface implementatie. Eerst worden de private field en de publieke property gedefinieerd. De publieke getter en de private setter van de property gedragen zich als methodes en die voeren code uit dus die code moet gedefinieerd worden via opcodes. Opcodes zijn de MSIL instructies waarmee bijvoorbeeld doorgegeven parameters op de stack worden geplaatst. Ook worden methode calls uitgevoerd die dan van de stack lezen en er weer op terugschrijven (zie http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes_members.aspx voor details over de hier gebruikte opcodes).
De derde stap is het aanmaken van de constructor voor de doorgifte van IWrappedInterface. De property met de ingepakte klas wordt zo eenmailg gevuld en daarna afgeschermd voor overschrijven.
Als laatste stap moeten alle methodes van IWrappedInterface doorgelust worden. Voor iedere te ondersteunen methode moeten alle door te geven parameters op de stack geplaatst worden en daarna wordt de methode van het ingepakte object aangeroepen om de logica uit te voeren (zie listing 4). Hiervoor maken we gebruik van de getter van de property. Een geretourneerde waarde uit de aanroep naar de methode wordt gewoon weer terug op de stack gezet.
|
MethodInfo[] methodInfosWrappedInterface = typeOfWrappedInterface.GetMethods();
foreach (MethodInfo methodInfoInterfaceType in methodInfosWrappedInterface) { //Put the parameter types in an array Type[] parameterTypes = new Type[ methodInfoInterfaceType.GetParameters().Length];
ParameterInfo[] pia = methodInfoInterfaceType.GetParameters(); for (int i = 0; i < pia.Length; i++) { parameterTypes[i] = pia[i].ParameterType; }
// Define the wrapper method for the wrapped method MethodBuilder methodBuilder = typeBuilder.DefineMethod( methodInfoInterfaceType.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.NewSlot, methodInfoInterfaceType.ReturnType, parameterTypes);
ILGenerator iLGenerator = methodBuilder.GetILGenerator(); // Put 'this' on the stack iLGenerator.Emit(OpCodes.Ldarg_0); // Load the field on the stack iLGenerator.EmitCall( OpCodes.Call, methodBuilderGet, null);
// Put every parameter passed on the stack for (int j = 0; j < pia.Length; j++) { iLGenerator.Emit(OpCodes.Ldarg, j + 1); }
// Call the method of the wrapped object iLGenerator.Emit(OpCodes.Callvirt, methodInfoInterfaceType); // Ready and return the result iLGenerator.Emit(OpCodes.Ret); } |
Listing 4: Alle methodes overnemen en doorlussen
De MSIL generatie is afgerond, kijk nu nog eens naar Listing 3. Helemaal onderaan na de vierde stap wordt vanuit de typebuilder het type gecreëerd en bij MSIL generatie problemen (bijvoorbeeld onvoldoende of verkeerde parameters op de stack) zal hier een exception optreden. Als hier niet wordt geklaagd, dan hebben we nu het gewenste type van de wrapper class.
Om dit gegenereerde type te kunnen gebruiken moet de wrapper class geïnstancieerd worden waarbij de het ingepakte object aan de constructor meegegeven wordt (zie listing 5).
|
// Make an instance of the wrapped class IWrappedInterface wrappedclass = new WrappedClass();
// Create the type of the wrapper Type wrapperType = WrapperHelper.CreateWrapperType( typeof(IWrapperInterface), typeof(IWrappedInterface));
// Make an instance of the type of the wrapper IWrapperInterface wrapper = (IWrapperInterface)Activator.CreateInstance( wrapperType, new object[] { wrappedclass });
// Call the wrapper instance string returnValue = wrapper.MethodOne("It is a wrap!"); Console.WriteLine(returnValue); |
Listing 5: Wrapper creëren en aanroepen
De gegenereerde wrapper wordt via een activator in een runtime object omgezet welke een IWrappedInterface inpakt.
Runtime wrapper generatie met RealProxy
Het inpakken kan ook zonder MSIL generatie door gebruik van de RealProxy (zie listing 6).
|
public class RealProxyWrapper<T, U> : RealProxy { private U _wrappedInstance;
public RealProxyWrapper(U wrappedInstance):base(typeof(T)) { _wrappedInstance = wrappedInstance; }
public override IMessage Invoke(IMessage message) { //Extract information about the method to call IMethodCallMessage methodCallMessage = new MethodCallMessageWrapper( (IMethodCallMessage)message);
MethodBase methodBase = typeof(U).GetMethod( methodCallMessage.MethodBase.Name);
//Invoke the method on the instance to collect result object returnValue = methodBase.Invoke( _wrappedInstance, methodCallMessage.Args);
//Collect all information as if the method is called //on an instance with the wrapper interface ReturnMessage returnMessage = new ReturnMessage( returnValue, methodCallMessage.Args, methodCallMessage.ArgCount, methodCallMessage.LogicalCallContext, methodCallMessage);
return returnMessage; } } |
Listing 6: Wrapper creëren en aanroepen
Deze wrapper zal zich voordoen als een IWrapperInterface implementatie en iedere methode aanroep wordt doorgegeven aan de via de constructor verkregen IWrappedInterface implementatie. Het gebruikt intern de Invoke methode. Deze doet op zijn beurt een invoke maar daar omheen kan nog extra logica opgenomen worden zoals logging of tijdsduur metingen.
|
RealProxyWrapper<IWrapperInterface, IWrappedInterface> realProxyWrapper = new RealProxyWrapper <IWrapperInterface, IWrappedInterface>(wrappedclass);
IPlainService wrapper = realProxyWrapper.GetTransparentProxy() as IPlainService;
wrapper.MethodOne("It is a wrap!"); |
Listing 7: Wrapper creëren en aanroepen
Van de ReapProxyWrapper moet een runtime object gecreëerd worden welke op zijn beurt de wrapper levert rond het ingepakte runtime object.
Basis voor een WCF service locator
Hierboven zijn twee versies beschreven van wrappers. Met het gebruik van de wrappers is aangetoond dat werkelijk iedere willekeurige interface definitie met een andere interface definitie ingepakt kan worden. Deze kunnen dus ook een WCF service gaan inpakken (zie listing 8).
|
public interface IPlainService { string DoWork(); } … ChannelFactory<IWcfService> factory = new ChannelFactory<IWcfService>( new WSHttpBinding());
IWcfService serviceToWrap = factory.CreateChannel(new EndpointAddress( "http://localhost/WcfHost/Service.svc"));
//choose between uncommenting IPlainService wrapper = GetReflectionWrapper(serviceToWrap); //or IPlainService wrapper = GetProxyWrapper(serviceToWrap);
string answer = wrapper.DoWork();
factory.Close(); |
Listing 7: Wrapper creëren en aanroepen
We roepen hier een WCF service aan met een totaal andere interface (welke toevallig dezelfde methode deelt). Hiermee is de mogelijkheid ontstaan om zelf een service locator (ook wel ServiceProvider genoemd) te bouwen. Een aanroepende partij vraagt de service aan welke door een bepaalde interface type ondersteund wordt. De serviceprovider gaat hier naar op zoek, creëert de cliënt proxy en geeft deze cliënt proxy terug.
Hiermee is de mogelijkheid ontstaan om zelf een service locator te bouwen
Conclusie
Voor mij was het bouwen van de dynamische wrappers een aangename kennismaking met MSIL. Ik heb gemerkt dat de leercurve bij MSIL eerst redelijk stijl is maar als je eenmaal bezig bent, valt alles best te begrijpen. Een goede kennis van net .Net framework is wel een vereiste. De RealProxy variant is later ontworpen en lijkt sprekend op de MSIL variant maar is veel flexibeler in het dagelijks gebruik. Deze geniet bij onze projecten dan ook de voorkeur.
Toch wil ik nog een lans breken voor MSIL generatie. Met MSIL generatie zijn heel krachtige oplossingen te bouwen die de compiler wel maar C# normaal niet toelaat binnen de taalconstructie. Zo wordt het mogelijk om bv. properties, fields en methodes die normaal private gedefinieerd zijn, toch uit te lezen. Helaas is goede documentatie schaars maar met de juiste tools kan goed uitgezocht worden hoe bepaalde code constructies in MSIL gerepresenteerd moet worden.
Het is zeker de moeite waard om gegenereerde code eens met .Net Reflector van Lutz Roeder te openen (http://www.red-gate.com/products/reflector/) Met deze tool is het ook mogelijk om MSIL code naar andere .Net talen (C#, VB.Net, Chrome, Delphi, etc) te ‘reflecteren’. Via de plug-in techniek van .Net Reflector wordt deze lijst van talen regelmatig uitgebreid en zo is het inmiddels ook mogelijk om ter controle MSIL code om te zetten naar broncode met de ReflectionEmitLanguage plug-in.
Veel succes!
Deze blog is een aangepaste versie van een artikel in de SDN Magazine nr. 99 (nov. 2008) Zie ook www.sdn.nl
Share this | 721 keer bekeken | 4 reacties
Enclosure: http://adi.atosoriginblog.nl/static//enclosures/sandervandevelde_broncode_reflectionalswcfinpakpapier_.zip
robert mekking reageert, op December 1, 2008 om 09:09 (GMT +01:00):
@Leon,Mee eens, volgende code-blog van jou?
>:-)
Leon Zandman reageert, op December 1, 2008 om 10:18 (GMT +01:00):
Wie kaatst kan de bal verwachten :-) Die opmerking had ik natuurlijk al verwacht.Sander van de Velde reageert, op December 4, 2008 om 16:12 (GMT +01:00):
<reclame>Blader nog eens door mijn vorige bijdrages heen. Er is voldoende code/leesvoer :-)</reclame>




Leon Zandman reageert, op November 30, 2008 om 23:52 (GMT +01:00):
Interessant! En eindelijk eens wat code op ons blog in plaats van al dat semi-academisch geneuzel, gna gna gna... ;-)