Friday, March 02, 2007

Writing a raw web service using an HttpHandler

The ASP.NET web service infrastructure provides a RPC style abstraction for web serivces. You write a web method that looks like any other C# function and simply add a [WebMethod] attribute. ASP.NET then reflects on the name of the method, your input parameters and return types and creates a web service end point for you. If you use the 'Add Web Reference' facility for the client, you never have to consider what the web service will look like in terms of WSDL or XML, it just looks like a remote procedure call. This is fine in many scenarios, but sometimes what you are interested in is the actual XML message itself and you don't need it deserialized as .net types. It's pretty straightforward to write a web service using an HttpHandler to grab the raw SOAP envelope. The IHttpHandler interface is the core endpoint interface for anything that handles a web request in .net. All you need to do is write a class that implements the interface and then register it in Web.config:
<httpHandlers>
<add verb="*" path="MyServiceEndpoint.asmx" type="MyService.MyHandler, MyHandler" />
</httpHandlers>
IHttpHandler provides a method 'PrcessRequest' that looks like this:
void ProcessRequest(HttpContext context);
You take the HttpContext.Request.InputStream and read the raw SOAP envelope from it. When you're ready to return the response, simply write your SOAP envelope response to the HttpContext.Response.OutputStream. A couple of other things to consider are providing a default information page and your web service's WSDL file. In ASP.NET if you execute an HTTP GET request against your web service, ASP.NET builds a nice web page naming your service and listing the operations. If you append WSDL to the querystring, it builds and returns the WSDL for the web service. You can replicate this behaviour by looking for a GET request (web service requests are typically POST) and then streaming either a descriptive web page or a WSDL document to the response. Here are functions that look for a GET request and stream either the WSDL file or the default page file into the response.
public void ProcessRequest(HttpContext context)
{
switch(context.Request.HttpMethod)
{
 case "GET":
  ProcessGetRequest(context);
  break;
 case "POST":
  ProcessPostRequest(context);
  break;
 default:
  throw new ApplicationException(
   string.Format("HTTP Method '{0}' is not supported.", context.Request.HttpMethod));
}
}

void ProcessGetRequest(HttpContext context)
{
// is this a request for the WSDL file?
string filePath;
if(context.Request.QueryString.ToString().ToUpper() == "WSDL")
{
 filePath = context.Server.MapPath(myWsdlPath);
}
else
{
 filePath = context.Server.MapPath(myDefaultPagePath);
}
RespondWithFile(context, filePath);
}

void RespondWithFile(HttpContext context, string filePath)
{
using(FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
 byte[] buffer = new byte[streamBufferSize];
 int bytes;
 while((bytes = fileStream.Read(buffer, 0, streamBufferSize)) > 0)
 {
  context.Response.OutputStream.Write(buffer, 0, bytes);
 }
 context.Response.OutputStream.Flush();
}
}
This technique can be usefull when you're writing stuff to route or map web services. Currently I'm working on a mapping service that takes a request and forwards it on to another web service with a different schema. Using the handler I can simply grab the SOAP envelope and run it through an XSLT to transform it to target schema and then run the response through another XSLT to transform it back to the originating web service's response schema.

5 comments:

  1. Hi Mike,
    Where you able to get all the operations using the custom handler?
    like - handling the raw SOAP request and complex type parameter handling..etc.

    I am trying to do the same but stuck, when I call the default handler provided by .NET, i.e. WebServiceHandlerFactory.GetHandler()..
    The executing fails to resolve the type...and give error "Error creating type 'mytype' ".

    I have tried AssemblyResolve nad TypeResolve event, but those are never reached :(

    Let me know if you have a idea on this?

    ReplyDelete
  2. Hi Bugs, it's been a while since I was working on this so I'm sorry if my memory is a little hazy. I cover handling complex type parameters in other posts on this blog. The technique is to build an in-memory model of your complex type (you can use xsd.exe for this) and then use the XML serializer to convert to and from the SOAP request. Of course if you don't know what the type is until you receive the request you're going to have a lot more fun. You will need to parse the type as XML using SAX or DOM.

    I don't quite understand what you mean when you say you are calling the default handler. The point of this post is to supply a custom handler to handle requests to a particular endpoint. Why are you accessing WebServiceHandlerFactory?

    ReplyDelete
  3. Hi Mike,
    Thanks for your reply. I am working on dynamic generation and invocation of web-serivce.

    So i wrote a class which inherited the IHttpHandlerFactory, instead of IHttpHandler..In that class I use to check if the web-service exits, if not I use CodeDOM to generate and compile it to some particular directory (say \services). The after creating the files I used Reflection to dynamically load the .dll of web service into the current appdomain.

    Now that the assembly and type is present in the appdoamin, i just call "GetCoreHandler" on the default WebServiceHandlerFactory class using Reflection. And pass the assembly anf type information to it. This works great in windows, but not on mono :(
    Mono does not implement "GetCoreHandler", so the CLR complaint method which I can invoke is WebServiceHandlerFactory.GetHandler, but this fails to resolve my type from the assembly ( which is already loaded in the appdoamin).

    This whole thing brought me to your post :)

    So now I want to implement the ProcessPostMessage() part. I am able to get the SOAP envelope, but I dont know how to move ahead..

    One thing is that i can use some Xml parser and manually check which method and parameters are passed to me in the soap message

    Seocnd I thought of first checking on what MS framework does with the raw SOAP envelope ..like what class are used to deserialize ..etc.

    I hope now you know what I am doing :)

    So, any sort of help is welcome.

    ReplyDelete
  4. Hi Bugs,

    If I understand you, you're trying to interpret any unknown SOAP request that is fired at your meta-service and then map the calls onto some API. Is that right?

    So the problem you have is that you've only got the SOAP request, not the WSDL.

    The ASP.NET web service stuff (not WCF) knows the schema of the SOAP message and simply uses the XML serializer to deserialize it to a .NET object graph. It gets the operation from the HTTP header. Simple.

    You've got a different problem, you have to understand the SOAP message without its WSDL. You can read the operation from the HTTP header, but for the request itself you'll probably have to use SAX or DOM to read it and then have some logic to work out what you want to do with it.

    It shouldn't be so hard, you just need to have a good understanding of how a SOAP request is structured, and there's plenty of documentation out there on that.

    ReplyDelete
  5. Hi Mike,
    The good thing is that I have the WSDL , the ServiceDescription object in memory, this is there because I am also processing it ?WSDL request from my http handler.

    the code in post handler is something like this :

    string soapmsg = (new StreamReader(context.Request.InputStream)).ReadToEnd();

    //here is get the raw soap xml envelope...

    i know i am executing a file .test.asmx, and the type and assembly is inside appdomain.

    So how to move futher with the 'soapmsg' is there any class to deserialise this directly to some class (like SoapMessage or SoapEnvelope).
    Or what should I deserialise this 'soapmsg' xml into? 'test' class or what?

    I tried using SoapFormatter but it fails. :(

    ReplyDelete

Note: only a member of this blog may post a comment.