Kijk hier voor het originele SDN artikel.
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 http://www.sdn.nl/