Click here to Skip to main content
15,861,125 members
Articles / Web Development / ASP.NET

Assembly-based webservice and Javascript proxy using reflection

Rate me:
Please Sign up or sign in to vote.
4.88/5 (17 votes)
30 May 2007CPOL6 min read 67.5K   326   33   17
ASP.NET and Ajax Webservices, not from a .asmx file, but from a compiled assembly with a little bit of reflection

Introduction

Anyone who has been building .NET application frameworks, custom controls or just likes to separate his concerns, will have come across the same problem I was having. I hope I can help some of you without having to go to the lengths I did, but let me start with explaining the problem. It's a pretty long story so hold on; the level in this article ranges from beginner to advanced in a lot of different areas.

The problem

When you want to develop Custom Controls with Javascript call-back functionality, you can use the ICallBack and IScriptable interface. These interfaces implement a call from your class and a return call to your class by use of Javascript, without having to implement an update panel or page refresh. The ICallback interface has a problem, though: it has no nice JSON (De)Serialization. It's confined to one function per component and although implementing more is possible, it is not very manageable.

How it should be

If you have ever worked with Webservices in ASP.NET, you will have seen the power of this mechanism. This is especially the case when in combination with ASP.NET Ajax 1.0, which generates a Javascript proxy for you. For those of you who haven't used ASP.NET Webservices, here's a small recap: You create an ASMX file in your project, fill it with a class containing a few attributes meant for Webservices and you are done! No SOAP creation, no JSON (DE)Serialization, no proxy writing. The only thing you still have to do is to register it with your <ScriptManager> for it to generate the Javascript Proxy. Would this not be great to have in our custom control library?

Yet another problem

The big problem with this approach is that this functionality only works with not-yet-compiled ASMX files in the web directory. You have no "out of the box" way of pointing this functionality to a compiled assembly. To take on this problem, we first take a look at how the current functionality works.

How it works by default

The ASMX files are translated into a web service and Javascript proxy through an HTTPHandler. This HTTPHandler is set in the config.xml file.

XML
<httpHandlers>
    <remove verb="*" path="*.asmx"/>
    <add verb="*" path="*.asmx" validate="false" 
        type="System.Web.Script.Services.ScriptHandlerFactory"/>
</httpHandlers>

The handler you see here is the one that is created by default in a new "ASP.NET Ajax-enabled Web Application." When a request is done for a file with the extension ASMX ("*.asmx"), it will forward that Request to the ScriptHandlerFactory. The scripthandler factory then creates the web service, Javascript Proxy or Javascript Method handler for you. When you add the web service to the ScriptManager, it will generate a <script src="Webserver.asmx/js"/> on your webpage. The problem with this class is that it will check for the ASMX file and nothing else. The class has a bigger problem, in that almost everything is Internal and Private. If you don't know what that means, it means that extending or changing anything is impossible.

Reflect it

Okay, maybe it's not impossible. Everything compiles into managed code and, with reflection, we can get into every little corner we like. Even private constructors can be reached through one of the most powerful components of the .NET Framework. There are a lot of tutorials out there about Reflection, so in this article I'm going to assume you know about the basics.

Beginning the solution

We start out with a new empty "ASP.NET AJAX-enabled Web application," which comes with ASP.NET Ajax 1.0. Then we add a second empty C# class library project as a second project and we reference the second project in the first.

AutoDiscoveryWebServiceHandlerFactory

To implement our own code, we are going to create our own HttpHandlerFactory in our CustomControl Library. We name it AutoDiscoveryWebServiceHandlerFactory with the interface IHttpHandlerFactory, which implements :

C#
public IHttpHandler GetHandler(HttpContext context, 
    string requestType, string url, string pathTranslated)

and

C#
public void ReleaseHandler

Then in the Web Application, we change the config.xml file to forward ASMX requests to our own handler factory.

XML
<httpHandlers>
    <remove verb="*" path="*.asmx"/>
    <add verb="*" path="*.asmx" validate="false" 
        type="Devusion. Services.AutoDiscoveryWebServiceHandlerFactory"/>
</httpHandlers>

Custom web service

Next we create our own web service, not the normal ASMX file, but a normal C# class file.

C#
namespace Devusion.CustomControls
{
    [ScriptService]
    [WebService(Namespace = "<a href="%22http://www.devusion.nl/xmlns/customcontrols%22">http://www.devusion.nl/xmlns/customcontrols</a>")]
    public class Service : WebService
    {
        [WebMethod]
        [ScriptMethod(UseHttpGet = true)]
        public string HelloWorldYou(string name)
        {
            return DateTime.Now.ToLongTimeString()+ ": Hello " + name;
        }
    }
}

The WebServiceHandlerFactory

Now let's write the code that makes it possible to call /Devusion.CustomControls.Service.asmx. This will call our AutoDiscoveryWebServiceHandlerFactory.GetHandler function. Here we have to translate the URL to the Type of the class. We do this by looping over the loaded assemblies and checking for web service attributes. This should be optimized for better performance. If we find no matching type, we will return null.

C#
private Type GetServiceType(string TypeName)
{
    // TODO: Caching mechanism for assembly checks
    foreach (
        Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies())
    {
        Type ClassType = loadedAssembly.GetType(TypeName);
        if (ClassType != null)
            return ClassType;
    }
    return null;
}

In Gethandler, we write:

C#
// Try to get the type associated with the request (On a name to type basis)
Type WebServiceType = 
    this.GetServiceType(Path.GetFileNameWithoutExtension(pathTranslated));

// if we did not find any send it on to the 
// original ajax script service handler.
if (WebServiceType == null)
{
    // [REFLECTION] Get the internal class 
    // System.Web.Script.Services.ScriptHandlerFactory create it
    IHttpHandlerFactory ScriptHandlerFactory = 
        (IHttpHandlerFactory)System.Activator.CreateInstance(
        AjaxAssembly.GetType(
        "System.Web.Script.Services.ScriptHandlerFactory"));
    UsedHandlerFactory = ScriptHandlerFactory;
    return ScriptHandlerFactory.GetHandler(
        context, requestType, url, pathTranslated);
}

The code here will get the WebServiceType. If no type is found, it will forward the request to the default Ajax Framework ScriptHandlerFactory, using reflection because it is a internal class. Almost all functionality for the ScriptHandlerFactory that normally handles the web service and Javascript proxy are either private or internal. Note that most of the functionality you will see from here on was found using the Reflector.net tool. The web service handler can be accessed with 3 different requests:

  1. A normal web service Method request (/Devusion.CustomControls.Service.asmx?......)
  2. A Javascript Proxy Generation request (/Devusion.CustomControls.Service.asmx/js or /jsdebug)
  3. A Javascript method Call ( /Devusion.CustomControls.Service.asmx/method )

First, we check for a normal or Javascript request:

C#
// [REFLECTION] get the Handlerfactory : RestHandlerFactory
// (Handles Javascript proxy Generation and actions)
IHttpHandlerFactory JavascriptHandlerFactory = 
    (IHttpHandlerFactory)System.Activator.CreateInstance(
    AjaxAssembly.GetType("System.Web.Script.Services.RestHandlerFactory"));

// [REFLECTION] Check if the current request is a Javasacript request
// JavascriptHandlerfactory.IsRestRequest(context);
System.Reflection.MethodInfo IsScriptRequestMethod = 
    JavascriptHandlerFactory.GetType().GetMethod("IsRestRequest", 
    BindingFlags.Static | BindingFlags.NonPublic);
if ((bool)IsScriptRequestMethod.Invoke(null, new object[] { context }))
{
    // Javascript Request
}
else
{
    // Normal Request
}

Then for the Javascript request we check for a proxy request or a method request:

C#
// Check and see if it is a Javascript Request or a request for a 
// Javascript Proxy.
bool IsJavascriptDebug = 
    string.Equals(context.Request.PathInfo, "/jsdebug", 
    StringComparison.OrdinalIgnoreCase);
bool IsJavascript = string.Equals(context.Request.PathInfo, "/js", 
    StringComparison.OrdinalIgnoreCase);
if (IsJavascript || IsJavascriptDebug)
{
    // Proxy Request
}
else
{
    // Method Request
}

Now for actions in the 3 request types.

The JavaScript proxy request

Here we do a lot more reflection; every reflection is explained in the comments. We create a WebServiceData with WebServiceType, which will then contain all of the information needed to create the web service. We directly use this object in the GetClientProxyScript method, which is a method of the object WebServiceClientProxyGenerator. It then returns the Javascript proxy.

C#
// [REFLECTION] fetch the constructor for the WebServiceData Object
ConstructorInfo WebServiceDataConstructor = AjaxAssembly.GetType(
    "System.Web.Script.Services.WebServiceData").GetConstructor(
    BindingFlags.NonPublic | BindingFlags.Instance, null, 
    new Type[] { typeof(Type), typeof(bool) } , null);

// [REFLECTION] fetch the constructor for the WebServiceClientProxyGenerator
ConstructorInfo WebServiceClientProxyGeneratorConstructor = 
    AjaxAssembly.GetType(
    "System.Web.Script.Services.WebServiceClientProxyGenerator").GetConstructor(
    BindingFlags.NonPublic | BindingFlags.Instance, null, 
    new Type[] { typeof(string), typeof(bool) }, null);

// [REFLECTION] get the method from WebServiceClientProxy to 
// create the Javascript : GetClientProxyScript
MethodInfo GetClientProxyScriptMethod = 
    AjaxAssembly.GetType(
    "System.Web.Script.Services.ClientProxyGenerator").GetMethod(
    "GetClientProxyScript", BindingFlags.NonPublic | BindingFlags.Instance, 
    null, new Type[] { AjaxAssembly.GetType(
    "System.Web.Script.Services.WebServiceData") }, null);

// [REFLECTION] We invoke : 
// new WebServiceClientProxyGenerator(url,false).GetClientProxyScript(
new WebServiceData(WebServiceType));
string Javascript = (string)GetClientProxyScriptMethod.Invoke(
    WebServiceClientProxyGeneratorConstructor.Invoke(
    new Object[] { url, IsJavascriptDebug }), new Object[] 
    {
        WebServiceDataConstructor.Invoke(
            new object[] { WebServiceType, false })
    }
);

Then for a little caching, this was almost literally taken from the original unit with Reflector.

C#
// Check the assembly modified time and use it as caching http header
DateTime AssemblyModifiedDate = GetAssemblyModifiedTime(
    WebServiceType.Assembly);

// See "if Modified since" was requested in the http headers, 
// and check it with the assembly modified time
string s = context.Request.Headers["If-Modified-Since"];

DateTime TempDate;
if (((s != null) && DateTime.TryParse(s, out TempDate)) && (
    TempDate >= AssemblyModifiedDate))
{
    context.Response.StatusCode = 0x130;
    return null;
}

// Add HttpCaching data to the http headers
if (!IsJavascriptDebug && (
    AssemblyModifiedDate.ToUniversalTime() < DateTime.UtcNow))
{
    HttpCachePolicy cache = context.Response.Cache;
    cache.SetCacheability(HttpCacheability.Public);
    cache.SetLastModified(AssemblyModifiedDate);
}

Then we need to get the Javascript Proxy String back to the client trough an HTTPHandler.

C#
internal class JavascriptProxyHandler : IHttpHandler
{
    string Javascript = "";

    public JavascriptProxyHandler(string _Javascript)
    {
        Javascript = _Javascript;
    }
    bool IHttpHandler.IsReusable { get { return false; } }
    void IHttpHandler.ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "application/x-Javascript";
        context.Response.Write(this.Javascript);
    }
}

Then we create and return in in the GetHandler code:

HttpHandler = new JavascriptProxyHandler(Javascript);
return HttpHandler;

That's it for the Javascript proxy. Let's get on with the other, simpler two.

The JavaScript method request

When you use the methods generated in the Javascript Proxy, it will access the web service with /methodname. The reason this handler is different from the normal web service handler (SOAP/XML), is that this interface needs to translate between Javascript JSON in our C# Methods.

C#
IHttpHandler JavascriptHandler = 
    (IHttpHandler)System.Activator.CreateInstance(
    AjaxAssembly.GetType("System.Web.Script.Services.RestHandler"));

// [REFLECTION] fetch the constructor for the WebServiceData Object
ConstructorInfo WebServiceDataConstructor = AjaxAssembly.GetType(
    "System.Web.Script.Services.WebServiceData").GetConstructor(
    BindingFlags.NonPublic | BindingFlags.Instance, null, 
    new Type[] { typeof(Type), typeof(bool) } , null);

// [REFLECTION] get method : JavaScriptHandler.CreateHandler
MethodInfo CreateHandlerMethod = JavascriptHandler.GetType().GetMethod(
    "CreateHandler", BindingFlags.NonPublic | BindingFlags.Static, null, 
    new Type[] { AjaxAssembly.GetType(
    "System.Web.Script.Services.WebServiceData"), typeof(string) }, null);

// [REFLECTION] Invoke CreateHandlerMethod :
// HttpHandler = JavaScriptHandler.CreateHandler(WebServiceType,false);
HttpHandler = (IHttpHandler)CreateHandlerMethod.Invoke(
    JavascriptHandler, new Object[]
    {
        WebServiceDataConstructor.Invoke(
        new object[] { WebServiceType, false }), 
        context.Request.PathInfo.Substring(1)
    }
);
return HttpHandler;

We create the JavascriptHandler and, like in the previous handler, the WebServiceData. Then we call CreateHandler with the WebServiceData as Parameter, which returns the correct HTTPHandler.

Normal web service request

The normal request is simpler. The class is public and only the function that receives WebServiceData instead of a filepath is private. We use this function to get another HttpHandler and return it.

C#
// Remember the used factory for later in ReleaseHandler
IHttpHandlerFactory WebServiceHandlerFactory = new WebServiceHandlerFactory();
UsedHandlerFactory = WebServiceHandlerFactory;

// [REFLECTION] Get the method CoreGetHandler
MethodInfo CoreGetHandlerMethod = UsedHandlerFactory.GetType().GetMethod(
    "CoreGetHandler", BindingFlags.NonPublic | BindingFlags.Instance);

// [REFLECTION] Invoke the method CoreGetHandler :
// WebServiceHandlerFactory.CoreGetHandler(WebServiceType,context,
    context.Request, context.Response);
HttpHandler = (IHttpHandler)CoreGetHandlerMethod.Invoke(UsedHandlerFactory, 
    new object[]
{
    WebServiceType, context, context.Request, context.Response
}
);
return HttpHandler;

Default.aspx

Now to see it all in action. First, we add a web service reference to the ScriptManager:

ASP.NET
<asp:ScriptManager ID="ScriptManager1" runat="server" >
    <Services>
        <asp:ServiceReference Path="Devusion.CustomControls.asmx" />
    </Services>
</asp:ScriptManager>

Below it, we put:

ASP.NET
<script type="text/Javascript">
    function RunHelloWorld(name)
    {
        name = prompt("Please enter your name :","John Doe");
        Devusion.CustomControls.Service.HelloWorldYou(name,
            HelloWorldDone,WebServiceCallFailed,'');
    }
    
    function HelloWorldDone(result,context,senderFunction)
    {
        alert(result);
    }
    
    function WebServiceCallFailed(error,context,senderFunction)
    {
        alert(error.get_message());
    }
</script>
<input type="button" onclick="RunHelloWorld();" value="Click Here" />

You can now place a breakpoint in the HelloWorldYou method in the WebService class in the service.cs file. Just run the page and let the magic of reflection do the rest! In this example, I did not implement the actual custom control or the preferred script# implementation of all the Javascript in combination with some Silverlight. I will divulge on that in future articles.

Demo code

The attached ZIP is a complete demo solution. It should open and build directly, so let me know if there are any problems. The code was created using Visual Studio 2005 Professional and ASP.NET Ajax 1.0.

History

  • 30 May, 2007 - Original version posted

License

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


Written By
Founder Devusion
Netherlands Netherlands
Robertjan is an independent Developing and usability Consultant.

He has been a speaker at CodeCamp, TechDays and other venues. And blogs regularly on http://www.robertjantuit.nl

His specialaties are User Interfaces, Silverlight, WPF, Custom Controls and Software Architecture, Design and Agile development.

Comments and Discussions

 
QuestionGreat job! Pin
mctd30-Nov-11 6:11
mctd30-Nov-11 6:11 
GeneralSublime! Pin
jimiscott28-Jan-11 6:13
jimiscott28-Jan-11 6:13 
GeneralGreat job Pin
Akleinek21-Feb-10 22:57
Akleinek21-Feb-10 22:57 
QuestionLicensing Pin
Ruh Chauhan25-Nov-08 11:05
Ruh Chauhan25-Nov-08 11:05 
AnswerRe: Licensing Pin
Robertjan Tuit25-Nov-08 11:08
Robertjan Tuit25-Nov-08 11:08 
GeneralBest Article In a Long Time Pin
rodmj20-May-08 9:03
rodmj20-May-08 9:03 
GeneralRe: Best Article In a Long Time Pin
Jorge Bay Gondra5-Sep-08 2:42
Jorge Bay Gondra5-Sep-08 2:42 
GeneralExcellent, thanks Pin
pdelprat22-Jan-08 1:39
pdelprat22-Jan-08 1:39 
GeneralRe: Excellent, thanks Pin
Robertjan Tuit22-Jan-08 1:48
Robertjan Tuit22-Jan-08 1:48 
GeneralRe: Excellent, thanks Pin
pdelprat22-Jan-08 2:54
pdelprat22-Jan-08 2:54 
GeneralRe: Excellent, thanks Pin
pdelprat22-Jan-08 2:32
pdelprat22-Jan-08 2:32 
Generalextremely useful Pin
James Ashley27-Dec-07 9:35
James Ashley27-Dec-07 9:35 
GeneralVery Nice Pin
naytron197511-Jun-07 9:48
naytron197511-Jun-07 9:48 
GeneralGreat (Was in need for that 1.5 years ago) Pin
Martin Bohring30-May-07 23:04
Martin Bohring30-May-07 23:04 
GeneralRe: Great (Was in need for that 1.5 years ago) Pin
Robertjan Tuit5-Jun-07 22:36
Robertjan Tuit5-Jun-07 22:36 
GeneralGreat! Pin
Steven Berkovitz30-May-07 9:37
Steven Berkovitz30-May-07 9:37 
GeneralRe: Great! Pin
Robertjan Tuit5-Jun-07 22:33
Robertjan Tuit5-Jun-07 22:33 

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

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