Thursday 16 May 2013

Creating an Lambda Method that creates an Asmx WebServices WebMethod invocation wrapper

After the Using AST to programmatically create an Asmx WebServices WebMethod invocation wrapper post, the next step is to create an API that helps in the creation of such ‘WebServices proxies’

Using the same technique described on the Creating a REPL editor that is linked to a Code editor post , here is a first code snippet with:
  • An WSDL generated C# file is loaded and assigned into the object fileAst
  • The type/class with the base class of System.Web.Services.Protocols.SoapHttpClientProtocol was assigned to the object wsClass
  • The Login WebService’s WebMethod was assigned to the object loginMethod
  • A new Ast Type (based on name of the wsClass) was added to the compilationUnit object, and assigned to the object wrapperType
  • The loginMethod was added to the wrapperType object
  • The CSharp representation of the compilationUnit object was created and showed in the codeEditor (with *.cs syntax colouring)
image

After executing the code above, the code editor (on the right hand-side of the GUI) looked like this:

image

For reference this is what my dev GUI looks like (at the moment):

image

(read the Using AST technology (from ICSharpCode.NRefactory) to script the creation of a C# file post for more details on how this works)

The next step is to add a Lambda method that will create the proxy method:

image

which creates this code:

image

One important item here is the use of .clone  in the create_ProxyMethod:

image

because without it:

image

any modification to the proxyMethod object, will  also affect the loginMethod (note how they are added to the wrapperType at different times)

image
(note that both method’s names is now NEW_NAME )

The next step is to remove the body of the proxyMethod (since we will need to rewrite it) and to (temporarily) add an extra _NEW to its name:

image

Which means that now we have an empty Login_NEW method:

image

Next we remove all attributes:

image

since the ones from the original Login method were part of the original WSDL-generated-C# file:

image

In order to reinvoke the original webservice we will need to create a new instance of the WebServices class:

image

and assign it to a variable called webservice (I also removed the original version of the Login method since we don’t need to see it anymore)

image

Now that we have an webService object we need to invoke its Login Method:

image

in this example with an primitive and an identifier:

image

Final step on to create a fully working method is to get the invocation details (name and parameters) from the proxy method:

image

so that the whole code looks like this:

image

and the generated code looks like this:

image

which is exactly what we want :)

And now that we have a generic method we can use it to create more proxies.

For example here is how to get all WebMethods and create proxies for the 2nd and 3rd method:

image

with the generated code now having 3 methods:

image

Now by looking at this code, we can see that it is probably over-the-top to have the WS_UserManagement object being created on every method, so we should moved that into a global property (with the object created on the class constructor).

Note: here is a good example of the power of extension methods. As I was writing this blog (and code) there wasn’t an extension method to create a property programmatically (using AST), so 'with them' this is the code that is needed:

image

to create a property that looks like this:

image

but if we add a new extension method called ast_Property to the O2 Platform

image

we can get the same result with a much simpler 'one liner':

image

For example, here are 3 new properties:

image 

which look like this in the generated code:

image

Back to the WebServices proxy code ...

Our next step is to:

  • add a property of the WS_UserManagement type, 
  • a constructor to the WS_UserManagement_Wrapper class and 
  • the assignment of a new instance of WS_UserManagement  to the property created

image

which (using extension methods) can be further reduced to:

image

and looks like this (in the right-hand side code editor):

image

Next we refactor this ‘create property and created it in the constructor’ in a Lambda method :

image

and after removing the (now redundant) proxy object creation from each proxied method, our code looks like this:

image 

with this ast-powered-code created:

image

One more important detail to handle is the case when the WebMethod returns null.

Although none of the current methods return void, this can be easily changed by using:

image

which will create this test code:

image

which will not compile:

image

To fix it, we need to detect if the return type of the method to proxy is null, and if it is, only add the invocation of the method (not the assignment into the result variable)

image

Now, if we create two proxy methods (the first modified to return void)

image

we get the correct code:

image

Note how inside Login there is only the invocation and inside  ListCurrentUsers the invocation is assigned to a variable, followed by it return;

We can now create all proxy methods in one go:

image

which is pretty cool:

image

There is only one final detail to do (for now) before we can use these in a live set-up.

We need to make these proxy methods into actual WebServices methods

This can be easily done by adding a reference to System.Web.Services:

image

and adding the WebMethod attribute to the proxied method:

image

which makes all proxied methods to look like this:

image

which is exactly what I wanted to do :)

Next post is about how this was actually used in a real world requirement.

Scripts used on this blog

1) create proxy method for Login WebMethod
   1: var compilationUnit = new CompilationUnit();
   2: var file = @"E:\O2\O2.Temp\5_10_2013\wsdl_tmpC5E1\WS_UserManagement.cs";
   3: var fileAst = file.csharpAst();
   4: var wsClass = fileAst.type_with_BaseType("System.Web.Services.Protocols.SoapHttpClientProtocol");             
   5:   
   6: var wrapperType  = compilationUnit.add_Type(wsClass.Name + "_Wrapper");
   7:  
   8: Func<MethodDeclaration,MethodDeclaration> create_ProxyMethod = 
   9:     (methodToProxy)=>{
  10:                         
  11:                         var proxyMethod = methodToProxy.clone()
  12:                                                        .remove_Attributes();
  13:                         var body = proxyMethod.add_Body();        
  14:                                                 
  15:                         var webService = body.add_Variable_with_NewObject("webService", wsClass);
  16:                                                 
  17:                         var parameters = methodToProxy.parameters().names().ast_Identifiers().ToArray();
  18:                         var invocation = "webService".ast_Invocation_onType(methodToProxy.Name,parameters);
  19:                         var result = body.add_Variable("result", invocation, methodToProxy.TypeReference);
  20:                         body.add_Return(result.Name.ast_Identifier());
  21:                         return proxyMethod;
  22:                      }; 
  23:  
  24: var loginMethod = wsClass.method("Login");
  25: var newMethod =  create_ProxyMethod(loginMethod);
  26:  
  27: wrapperType.append(newMethod);  
  28:  
  29: var csharpCode = compilationUnit.csharpCode()
  30:                                 .insertAt(0, @"//O2File:{0}".format(file).line());
  31: var tempFile = csharpCode.saveWithExtension(".cs");
  32: codeEditor.open(tempFile);
  33:  
  34: //if (false)
  35:     codeEditor.compileCSSharpFile()          
  36:               .executeFirstMethod();
  37:               
  38: //codeEditor.set_Text(compilationUnit.csharpCode(), ".cs");
  39: return "done";

2) script that creates all proxy methods (in a complete/working state)
   1: var compilationUnit = new CompilationUnit();
   2: var file = @"E:\O2\O2.Temp\5_10_2013\wsdl_tmpC5E1\WS_UserManagement.cs";
   3: var fileAst = file.csharpAst();
   4: var wsClass = fileAst.type_with_BaseType("System.Web.Services.Protocols.SoapHttpClientProtocol");
   5:              
   6: compilationUnit.add_Using("System"); 
   7: compilationUnit.add_Using("System.Web.Services"); 
   8:   
   9: var wrapperType  = compilationUnit.add_Type(wsClass.Name + "_Wrapper");
  10:  
  11: Action<TypeDeclaration, string, TypeDeclaration> add_Property_To_Type_Constructor = 
  12:     (targetType, propertyName, propertyType) =>
  13:         {
  14:             targetType.add_Property(propertyName,propertyType)    
  15:                       .add_Ctor()                                
  16:                       .body()                                    
  17:                       .add_Assignment(propertyName, propertyType);
  18:         
  19:         };
  20:  
  21: Func<MethodDeclaration,string, MethodDeclaration> create_ProxyMethod = 
  22:     (methodToProxy, webServicesObjectName) =>
  23:         {            
  24:             var proxyMethod = methodToProxy.clone()
  25:                                            .remove_Attributes();
  26:             var body = proxyMethod.add_Body();        
  27:             
  28:             proxyMethod.add_Attribute("WebMethod");
  29:             
  30:             var parameters = methodToProxy.parameters().names().ast_Identifiers().ToArray();
  31:             var invocation = webServicesObjectName.ast_Invocation_onType(methodToProxy.Name,parameters);
  32:             proxyMethod.TypeReference.name().info();
  33:             if(proxyMethod.TypeReference.name() =="void" || proxyMethod.TypeReference.name() == "System.Void")
  34:             {
  35:                 body.append(invocation.expressionStatement());
  36:             }
  37:             else
  38:             {
  39:                 var result = body.add_Variable("result", invocation, methodToProxy.TypeReference);
  40:                 body.add_Return(result.Name.ast_Identifier());
  41:             }
  42:             return proxyMethod;
  43:          }; 
  44:  
  45:  
  46: var propName = "_web_Service";
  47: add_Property_To_Type_Constructor(wrapperType,propName, wsClass);
  48:  
  49: var wsMethods = wsClass.methods_with_Attribute("System.Web.Services.Protocols.SoapDocumentMethodAttribute");
  50: foreach(var wsMethod in wsMethods)
  51: {
  52:     wrapperType.append(create_ProxyMethod(wsMethod,propName));      
  53: }
  54:  
  55:  
  56:  
  57: var csharpCode = compilationUnit.csharpCode()
  58:                                 .insertAt(0, @"//O2File:{0}".format(file).line());
  59: var tempFile = csharpCode.saveWithExtension(".cs");
  60: codeEditor.open(tempFile);
  61:  
  62: if (false)
  63:     codeEditor.compileCSSharpFile()          
  64:               .executeFirstMethod();
  65:               
  66: //codeEditor.set_Text(compilationUnit.csharpCode(), ".cs");
  67: return "done";
  68:   
  69: //using ICSharpCode.NRefactory.Ast
  70: //using O2.API.AST.ExtensionMethods.CSharp
  71: //O2File:_Extra_methods_To_Add_to_Main_CodeBase.cs