Click here to Skip to main content
Click here to Skip to main content

Generate a client-side proxy for a WebService using HTTP Handlers, Mootools, and JSON

, 19 Feb 2007
Rate this:
Please Sign up or sign in to vote.
We will create code that will generate all the JavaScript necessary to call a WebService, sending and recieving JSON. This will allow us to choose which JavaScript library (such as Mootools, prototype, scriptaculous, etc...) to use and still be able to perform this task.

Introduction

With the release of the MSFT AJAX Extensions, calling a Web Service from client-side is a kid's task.

But what if you, like me, want to call a Web Service but don't want to use the AJAX Extensions, using instead another library, like MooTools? Well, you could *just* create the SOAP body and send it to the Web Service. That's seems easy, right?

Well, I like things that generate themselves.

In this post, I will create a simple client-side proxy from a Web Service, and if all ends well, we will be able to call it and get a response.

Background info

For understanding how this should be done, I went and "reflected" the MSFT AJAX Extensions assemblies to see how they got this to work. So some of the code presented in this proof of concept is based on that. Again, the main idea is to understand how to build a proxy similar to the one used by the MSFT AJAX Extensions, but without really using it.

Why not use the MSFT AJAX Extensions

Well, first of all, I wanted to learn how the whole process worked.

I also wanted to be able to call a Web Service by sending and receiving JSON without using the MSFT AJAX Extensions. Many small sized libraries make XHR calls. Why not use them?

Another issue, not covered here, is the usage of this code (with some slight changes) on the v1.1 of the .NET Framework.

The first thing...

... that we need to do is understand the life cycle of this:

Given a Web Service (or a list of Web Services), the application will validate if the Web Service has the [AjaxRemoteProxy] attribute. If so, we will grab all the [WebMethod] methods that are public and generate the client-side proxy. When the client-proxy is called, on the server, we need to get the correct method, invoke it, and return its results "JSON style". All of this server-side is done with some IHttpHandlers.

A HandlerFactory will do the work on finding out what is needed: the default Web Service handler, a proxy handler, or a response handler.

The proxy file will be the ASMX itself, but now, we will add a "/js" to the end of the call, resulting in something like this:

<script src="http://www.codeproject.com/ClientProxyCP/teste.asmx/js" 
       type="text/javascript"></script>

When the call is made to this, a handler will know that JavaScript is needed, and generate it.

Show me some code

The first thing we need to have is the AjaxRemoteProxy attribute. This attribute will allow us to mark which Web Services and web methods we will be able to call on the client side:

using System;

namespace CreativeMinds.Web.Proxy
{
    [AttributeUsage(AttributeTargets.Class | 
     AttributeTargets.Method, AllowMultiple = false)]
    public class AjaxRemoteProxyAttribute : Attribute
    {
        #region Private Declarations
        private bool  _ignore = false;
        #endregion Private Declarations

        #region Properties
        /// <summary>
        /// Gets or sets a value indicating whether the target is ignored.
        /// </summary>
        /// <value><c>true</c> if ignore;
        /// otherwise, <c>false</c>.</value>
        public bool  Ignore
        {
            get { return _ignore; }
            set { _ignore = value; }
        }
        #endregion Properties

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the
        /// <see cref="AjaxRemoteProxyAttribute"/> class.
        /// </summary>
        public AjaxRemoteProxyAttribute()
        {
        }
        /// <summary>
        /// Initializes a new instance of the
        /// <see cref="AjaxRemoteProxyAttribute"/> class.
        /// </summary>
        /// <param name="_ignore">if set
        /// to <c>true</c> if we wish to ignore this target.</param>
        public AjaxRemoteProxyAttribute(bool _ignore)
        {
            this._ignore = _ignore;
        }
        #endregion Constructor
    }
}

Now that we have our attribute, let's create a simple Web Service:

using System.Web.Services;
using CreativeMinds.Web.Proxy;

namespace CreativeMinds.Web.Services{
    [AjaxRemoteProxy()]
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class MyWebService : WebService
    {
        [WebMethod]
        public string HelloWorld()
        {
            return "Hello World";
        }
        [WebMethod]
        public string HelloYou(string name)
        {
            return "Hello " + name;
        }
    }
}

Notice that the Web Service class is marked with our newly created attribute.

Now comes the cool code. The first thing we need to do is let the application know that the calls to *.asmx are now handled by us. So we need to do two things: first, create the handler, and then change the web.config file.

The WebServices Handler Factory

As it was said before, all *.asmx calls will be handled by us. Because we also want to maintain the normal functionality of the Web Services, we need to create a handler factory. This factory will manage the return of the specific handler based on the following assumptions:

  1. If context.Request.PathInfo ends with "/js", we need to generate the proxy;
  2. If context.Request.ContentType is "application/json;" or we have a context.Request.Headers["x-request"] with a "JSON" value, we need to execute a method and return its value;
  3. otherwise, we let the Web Service run normally.

So let's build our factory:

using System;
using System.Web;
using System.Web.Services.Protocols;

namespace CreativeMinds.Web.Proxy
{
    public class RestFactoryHandler:IHttpHandlerFactory
    {
        #region IHttpHandlerFactory Members

        public IHttpHandler GetHandler(HttpContext context, 
               string requestType, string url, string pathTranslated)
        {
            if (string.Equals(context.Request.PathInfo, "/js", 
                              StringComparison.OrdinalIgnoreCase))
            {
                return new RestClientProxyHandler();
            }
            else
            {
                if (context.Request.ContentType.StartsWith("application/json;", 
                            StringComparison.OrdinalIgnoreCase) || 
                    (context.Request.Headers["x-request"] != null && 
                    context.Request.Headers["x-request"].Equals("json", 
                            StringComparison.OrdinalIgnoreCase)))
                {
                    return new RestClientResponseHandler();
                }
            }
            return new WebServiceHandlerFactory().GetHandler(context, 
                       requestType, url, pathTranslated);
        }

        public void ReleaseHandler(IHttpHandler handler)
        {
            
        }

        #endregion
    }
}

Then, we also need to let the application know about our factory:

<httpHandlers>
    <remove verb="*" path="*.asmx"/>
    <add verb="*" path="*.asmx" validate="false" 
               type="CreativeMinds.Web.Proxy.RestFactoryHandler"/>
</httpHandlers>

The client-side proxy generator handler

When context.Request.PathInfo equals "/js", we need to generate the client-side proxy. For this task, the factory will return the RestClientProxyHandler.

using System.Web;

namespace CreativeMinds.Web.Proxy
{
    class RestClientProxyHandler : IHttpHandler
    {
        private bool isReusable = true;

        #region IHttpHandler Members

        ///<summary>
        ///Enables processing of HTTP Web requests by a custom
        ///HttpHandler that implements the 
        ///<see cref="T:System.Web.IHttpHandler"></see> interface.
        ///</summary>
        ///
        ///<param name="context">An <see 
        ///cref="T:System.Web.HttpContext"></see> 
        ///object that provides references to the intrinsic server objects 
        ///(for example, Request, Response, Session, and Server)
        ///used to service HTTP requests. </param>
        public void ProcessRequest(HttpContext context)
        {
            WebServiceData wsd = context.Cache["WS_DATA:" + 
                    context.Request.FilePath] as WebServiceData;
            if (wsd != null)
            {
                wsd.Render(context);
            }
        }

        ///<summary>
        ///Gets a value indicating whether another request can use 
        ///the <see cref="T:System.Web.IHttpHandler"></see> instance.
        ///</summary>
        ///
        ///<returns>
        ///true if the <see cref="T:System.Web.IHttpHandler">
        ///        </see> instance is reusable; otherwise, false.
        ///</returns>
        ///
        public bool IsReusable
        {
            get { return isReusable; }
        }

        #endregion
    }
}

Notice two things:

  1. The handler uses a WebServiceData object. This object contains information about the Web Service. What we do here is get the WebServiceData object from context.Cache and render it.
  2. context.Cache["WS_DATA:" + ... ] holds all the WebServiceData on all Web Services that are proxified. This collection is filled in the WebServiceData object.

WebServiceData object

As said, WebServiceData contains basic information about the Web Service. It is also responsible for the rendering and execution of the Web Service.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security;
using System.Text;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
using System.Web.Services;
using System.Web.UI;
using Newtonsoft.Json;

namespace CreativeMinds.Web.Proxy
{
    internal class WebServiceData
    {
        #region Private Declarations

        private List<MethodInfo> _methods;
        private Type _type;
        private string _wsPath;
        private object _typeInstance;

        #endregion Private Declarations

        #region Constructor

        public WebServiceData(string wsPath)
        {
            _wsPath = wsPath;
            _methods = new List<MethodInfo>();
            Process();
        }

        #endregion Constructor

        #region Process

        /// <summary>
        /// Processes this instance.
        /// </summary>
        private void Process()
        {
            //Verifies if the path to the webservice is valid
            if (HostingEnvironment.VirtualPathProvider.FileExists(_wsPath))
            {
                Type type1 = null;
                try
                {
                    // Lets try and get the Type from the Virtual Path
                    type1 = BuildManager.GetCompiledType(_wsPath);
                    if (type1 == null)
                    {
                        type1 = BuildManager.CreateInstanceFromVirtualPath(
                                                  _wsPath, typeof (Page)).GetType();
                    }

                    if (type1 != null)
                    {
                        // Good. We have a Type. Now lets check if this is to Profixy.
                        object[] objArray1 = type1.GetCustomAttributes(
                                   typeof (AjaxRemoteProxyAttribute), true);
                        if (objArray1.Length == 0)
                        {
                            throw new InvalidOperationException(
                                   "No AjaxRemoteProxyAttribute found on webservice");
                        }

                        // Well. So far so good.
                        // Let's get all the methods.
                        BindingFlags flags1 = BindingFlags.Public | 
                               BindingFlags.DeclaredOnly | BindingFlags.Instance;
                        MethodInfo[] infoArray1 = type1.GetMethods(flags1);

                        foreach (MethodInfo info1 in infoArray1)
                        {
                            // we only need the WebMethods
                            object[] metArray1 = 
                              info1.GetCustomAttributes(typeof (WebMethodAttribute), true);
                            if (metArray1.Length != 0)
                            {
                                _methods.Add(info1);
                            }
                        }
                        
                        // keep locally the Type
                        _type = type1;

                        // Add this WS to the Cache, for later use.
                        if (HttpContext.Current.Cache["WS_DATA:" + 
                                 VirtualPathUtility.ToAbsolute(_wsPath)] == null)
                        {
                            HttpContext.Current.Cache["WS_DATA:" + 
                                  VirtualPathUtility.ToAbsolute(_wsPath)] = this;
                        }
                    }
                    else
                    {
                        throw new ApplicationException("Couldn't proxify webservice!!!!");
                    }
                }
                catch (SecurityException)
                {
                }
            }
        }

        #endregion

        #region Render

        /// <summary>
        /// Renders the Proxy to the specified <see cref="HttpContext"/>.
        /// </summary>
        /// <param name="context">The
        ///    <see cref="HttpContext"/>.</param>
        public void Render(HttpContext context)
        {
            // Set the ContentType to Javascript
            context.Response.ContentType = "application/x-javascript";

            StringBuilder aux = new StringBuilder();
            if (_type == null) return;

            // Register the namespace
            aux.AppendLine(string.Format(
                "RegisterNamespace(\"{0}\");", _type.Namespace));

            // Create the Class for this Type
            string nsClass = string.Format("{0}.{1}", _type.Namespace, _type.Name);
            aux.AppendLine(string.Format("{0} = function(){{}};", nsClass));

            _methods.ForEach(delegate (MethodInfo method)
             {
                 // Create a static Method on the class
                 aux.AppendFormat("{0}.{1} = function(", nsClass, method.Name);

                 // Set the method arguments
                 StringBuilder argumentsObject = new StringBuilder();
                 foreach (ParameterInfo info2 in method.GetParameters())
                 {
                     aux.AppendFormat("{0}, ", info2.Name);
                     argumentsObject.AppendFormat("\"{0}\":{0}, ", info2.Name);
                 }

                 if (argumentsObject.Length > 0)
                 {
                     argumentsObject = 
                       argumentsObject.Remove(argumentsObject.Length - 2, 2);
                     argumentsObject.Insert(0, "{").Append("}");
                 }

                 // Add the CompleteHandler argument in last
                 aux.Append("onCompleteHandler){\n");

                 // Render the method Body with the XHR call
                 aux.AppendLine(string.Format("new Json.Remote(\"{1}\", 
                       {{onComplete: onCompleteHandler, method:'post'}}).send({0});", 
                       argumentsObject.ToString(), 
                       VirtualPathUtility.ToAbsolute(_wsPath + "/" + method.Name)));
                 aux.Append("}\n");
             });
            context.Response.Write(aux.ToString());
        }

        #endregion

        #region Invoke

        /// <summary>
        /// Invokes the requested WebMethod specified in the <see cref="HttpContext"/>.
        /// </summary>
        /// <param name="context">The <see cref="HttpContext"/>.</param>
        public void Invoke(HttpContext context)
        {
            // Method name to invoke
            string methodName = context.Request.PathInfo.Substring(1);

            // We need an Instance of the Type
            if (_typeInstance == null)
                _typeInstance = Activator.CreateInstance(_type);

            // Get the Posted arguments (format "json={JSON Object}")
            string requestBody = 
              new StreamReader(context.Request.InputStream).ReadToEnd();
            string[] param = requestBody.Split('=');
            // JSON Deserializer @ http://www.newtonsoft.com/products/json/
            object a = JavaScriptConvert.DeserializeObject(param[1]);
            //object a = JavaScriptDeserializer.DeserializeFromJson<object>(param[1]);
            Dictionary<string, object> dic = a as Dictionary<string, object>;
            int paramCount = 0;
            if (dic != null)
            {
                paramCount = dic.Count;
            }
            object[] parms = new object[paramCount];

            if (dic != null)
            {
                int count = 0;
                foreach (KeyValuePair<string, object> kvp in dic)
                {
                    Debug.WriteLine(string.Format("Key = {0}, Value = {1}", 
                                    kvp.Key, kvp.Value));
                    parms[count] = kvp.Value;
                    count++;
                }
            }

            // Get the method to invoke and invoke it
            MethodInfo minfo = _type.GetMethod(methodName);
            object resp = minfo.Invoke(_typeInstance, parms);

            // Serialize the response
            // JSON Serializer @ http://www.newtonsoft.com/products/json/
            string JSONResp = 
              JavaScriptConvert.SerializeObject(new JsonResponse(resp));

            // Render the output to the context
            context.Response.ContentType = "application/json";
            context.Response.AddHeader("X-JSON", JSONResp);
            context.Response.Write(JSONResp);
        }

        #endregion
    }

    /// <summary>
    /// Wrapper for the JSON response
    /// </summary>
    public class JsonResponse
    {
        private object  _result = null;

        /// <summary>
        /// Gets or sets the result output.
        /// </summary>
        /// <value>The result.</value>
        public object  Result
        {
            get { return _result; }
            set { _result = value; }
        }


        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="JsonResponse"/> class.
        /// </summary>
        /// <param name="_result">The _result.</param>
        public JsonResponse(object _result)
        {
            this._result = _result;
        }
    }
}

When initialized, the WebServiceData object will try to get a Type from the Web Service path. If successful, it will check if the Web Service has AjaxRemoteProxyAttribute, and if true, will extract the WebMethods list.

The Invoke method looks at context.Request.PathInfo to see what method to execute. It also checks if the arguments are passed on the context.Request.InputStream, and if so, adds them to the method call. In the end, the response is serialized into a JSON string and sent back to the client.

The Render method looks at all the WebMethods and creates the client-side code.

The JsonResponse class is used to simplify the serialization of the JSON response.

With this, we have completed the first big step: Build the necessary code to generate the proxy.

Now, to help up "proxifing" the WebServices, we will build a simple helper to use on the WebForms:

using System.Collections.Generic;
using System.Web;
using System.Web.UI;

namespace CreativeMinds.Web.Proxy
{
    public static class ProxyBuilder
    {
        #region Properties

        /// <summary>
        /// Gets or sets the get WS proxy list.
        /// </summary>
        /// <value>The get WS proxy list.</value>
        public static List<string> WSProxyList
        {
            get
            {
                List<string> aux = 
                  HttpContext.Current.Cache["WS_PROXIES_URL"] as List<string>;
                HttpContext.Current.Cache["WS_PROXIES_URL"] = 
                  aux ?? new List<string>();
                return HttpContext.Current.Cache["WS_PROXIES_URL"] as List<string>;
            }
            set
            {
                HttpContext.Current.Cache["WS_PROXIES_URL"] = value;
            }
        }

        #endregion Properties


        public static void For(string wsPath)
        {
            if (!WSProxyList.Exists(delegate(string s) { return s == wsPath; }))
            {
                new WebServiceData(wsPath);
                WSProxyList.Add(wsPath);
            }
        }

        /// <summary>
        /// Renders all Webservice Proxies in the <see cref="Page"/>.
        /// </summary>
        /// <param name="page">The <see 
        /// cref="Page"/> where the proxies will be generated and sused.</param>
        public static void RenderAllIn(Page page)
        {
            WSProxyList.ForEach(delegate(string virtualPath)
               {
                   string FullPath = 
                     VirtualPathUtility.ToAbsolute(virtualPath + "/js");
                   page.ClientScript.RegisterClientScriptInclude(
                       string.Format("WSPROXY:{0}", FullPath), FullPath);
               });
        }
    }
}

The ProxyBuilder.For method receives a string with the virtual path to the WebService. With a valid path, this method will add a new WebServiceData object to the WSProxyList property.

When no more proxies are needed, ProxyBuilder.RenderAllIn should be called. This will register all the client script generated by our proxies.

protected void Page_Load(object sender, EventArgs e)
{
    ProxyBuilder.For("~/teste.asmx");
    ProxyBuilder.RenderAllIn(this);
}

Browsing the page, we can now see the output for our Web Service:

RegisterNamespace("CreativeMinds.Web.Services");
CreativeMinds.Web.Services.teste = function(){};
CreativeMinds.Web.Services.teste.HelloWorld = function(onCompleteHandler){
    new Json.Remote("/CreativeMindsWebSite/teste.asmx/HelloWorld", 
                    {onComplete: onCompleteHandler, method:'post'}).send();
}
CreativeMinds.Web.Services.teste.HelloYou = function(name, onCompleteHandler){
new Json.Remote("/CreativeMindsWebSite/teste.asmx/HelloYou", 
   {onComplete: onCompleteHandler, method:'post'}).send({"name":name});
}

Sweet! The generated JavaScript resembles our WebService class. We have the namespace CreativeMinds.Web.Services created, and the class name teste is also there, and its Web Methods. Notice that all method calls need a onCompleteHandler. This will handle all the calls successfully.

Only two steps remaining: the Response Handler, and testing it all.

Response Handler

As you can see in the code generated by the proxy, the call to the WebService method doesn't change:

/CreativeMindsWebSite/teste.asmx/HelloWorld

So how can we know what to return - JSON or XML?. Well, we will watch for context.Request.ContentType and context.Request.Headers on our RestFactoryHandler class. If one of those has JSON on it, we know what to do... Smile | :)

When a JSON response is requested, RestFactoryHandler will return RestClientResponseHandler.

using System.Web;

namespace CreativeMinds.Web.Proxy
{
    public class RestClientResponseHandler : IHttpHandler
    {
        #region IHttpHandler Members

        ///<summary>
        ///Enables processing of HTTP Web requests by a custom HttpHandler 
        ///that implements the <see cref="T:System.Web.IHttpHandler"></see> interface.
        ///</summary>
        ///
        ///<param name="context">An 
        ///  <see cref="T:System.Web.HttpContext"></see> object that
        /// provides references to the intrinsic server objects
        /// (for example, Request, Response, Session, and Server)
        /// used to service HTTP requests. </param>
        public void ProcessRequest(HttpContext context)
        {
            WebServiceData wsd = context.Cache["WS_DATA:" + 
                       context.Request.FilePath] as WebServiceData;
            if (wsd != null)
            {
                wsd.Invoke(context);
            }
        }

        ///<summary>
        ///Gets a value indicating whether another request can use 
        ////the <see cref="T:System.Web.IHttpHandler"></see> instance.
        ///</summary>
        ///
        ///<returns>
        ///true if the <see cref="T:System.Web.IHttpHandler"></see>
        ///instance is reusable; otherwise, false.
        ///</returns>
        ///
        public bool IsReusable
        {
            get { return true; }
        }

        #endregion
    }
}

Again, notice that it tries to get a WebServiceData object from context.Cache and Invoke it passing the context as the argument. The Invoke method of WebServiceData will extract the method name form the PathInfo. Then, it will create an instance from the Type, and check for arguments passed on the post by checking Request.InputStream. Using Newtonsoft JavaScriptDeserializer, we deserialize any arguments and add them to the object collection needed to invoke a method. Finally, we invoke the method, serialize the response, and send it back to the client.

...

namespace CreativeMinds.Web.Proxy
{
    internal class WebServiceData
    {
        ...
        /// <summary>
        /// Invokes the requested WebMethod
        /// specified in the <see cref="HttpContext"/>.
        /// </summary>
        /// <param name="context">The <see cref="HttpContext"/>.</param>
        public void Invoke(HttpContext context)
        {
            // Method name to invoke
            string methodName = context.Request.PathInfo.Substring(1);

            // We need an Instance of the Type
            if (_typeInstance == null)
                _typeInstance = Activator.CreateInstance(_type);

            // Get the Posted arguments (format "json={JSON Object}")
            string requestBody = 
              new StreamReader(context.Request.InputStream).ReadToEnd();
            string[] param = requestBody.Split('=');
            // JSON Deserializer @ http://www.newtonsoft.com/products/json/
            object a = JavaScriptConvert.DeserializeObject(param[1]);
            //object a = JavaScriptDeserializer.DeserializeFromJson<object>(param[1]);
            Dictionary<string, object> dic = a as Dictionary<string, object>;
            int paramCount = 0;
            if (dic != null)
            {
                paramCount = dic.Count;
            }
            object[] parms = new object[paramCount];

            if (dic != null)
            {
                int count = 0;
                foreach (KeyValuePair<string, object> kvp in dic)
                {
                    Debug.WriteLine(string.Format("Key = {0}, Value = {1}", 
                                    kvp.Key, kvp.Value));
                    parms[count] = kvp.Value;
                    count++;
                }
            }

            // Get the method to invoke and invoke it
            MethodInfo minfo = _type.GetMethod(methodName);
            object resp = minfo.Invoke(_typeInstance, parms);

            // Serialize the response
            // JSON Serializer @ http://www.newtonsoft.com/products/json/
            string JSONResp = 
              JavaScriptConvert.SerializeObject(new JsonResponse(resp));

            // Render the output to the context
            context.Response.ContentType = "application/json";
            context.Response.AddHeader("X-JSON", JSONResp);
            context.Response.Write(JSONResp);
        }
        ...

With this, we are ready to test a call. All we need to do is, first create the onCompleteHandler function to handle the response:

function completedHandler(json)
{
    alert(json.Result);
}

Then add a textbox to the page:

<input type="textbox" id="txtName" />

Finally, a caller:

<a href="#" 
   onclick="CreativeMinds.Web.Services.teste.HelloYou(
            $("textbox").value, complete)">call HelloYou</a>

That's it. We have built a proxy generator.

Again, this is a proof of concept, so it's not tested for performance nor is bug/error proof.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Bruno R. Figueiredo
Web Developer
Portugal Portugal
No Biography provided

Comments and Discussions

 
GeneralMisleading title PinmemberDewey19-Feb-07 10:02 
GeneralRe: Misleading title PinmemberBruno R. Figueiredo19-Feb-07 11:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 19 Feb 2007
Article Copyright 2007 by Bruno R. Figueiredo
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid