Creating a General-Purpose, Asynchronous SOAP Client in ASP.NET 2.0
How's that for a blog-post title? There are a couple cool things shown within this post, highlighting 1 or 2 as a title was a little difficult.
A customer said that they have a testing org that needs to call web services and record the results. They want to be able to do this from their portal, which means ASP.NET 2.0 will be used to call the web service. I am going to show how to call a web service from an ASP.NET page asynchronously, and we will do so not by using WCF or ASMX, but with good ol' System.Net.WebRequest.
The first thing we'll set up is the UI. We will need two text boxes, one that allows us to tinker with the outgoing SOAP request and another that displays the incoming SOAP response. Since we will be putting XML into a textbox, we need to turn off ASP.NET 2.0's ValidateRequest feature for this page. Additionally, since we will be invoking operations asynchronously within an ASP.NET 2.0 Page class, we also need to set the Async="true" attribute in our Page directive. Here's our result:
<%@
Page
Language="C#"
ValidateRequest="false"
Async="true"
AutoEventWireup="true"
CodeFile="Default.aspx.cs"
Inherits="_Default"
%>
<!DOCTYPE
html
PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
>
<head
runat="server">
<title>Generic SOAP Tester</title>
</head>
<body>
<form
id="form1"
runat="server">
<div>
<asp:Label
runat="server"
ID="label1"
Text="Target URL:"
/>
<asp:TextBox
runat="server"
ID="targetUrl"
Width="295px"
Text="http://localhost:49186/WCFService8/Service.svc"
/>
<br
/>
<asp:Label
runat="server"
ID="Label2"
Text="SOAPAction:"
/>
<asp:TextBox
runat="server"
ID="soapAction"
Width="286px"
Text="http://tempuri.org/IUniversalContract/MyOperation1"
/>
<br
/>
<asp:TextBox
runat="server"
ID="TextBox1"
Rows="20"
TextMode="MultiLine"
Width="375px"
/>
<br
/>
<asp:Button
runat="server"
ID="Button2"
OnClick="Button2_Click"
Text="Call Service"
/><br
/>
<br
/>
<asp:TextBox
runat="server"
ID="TextBox2"
Rows="20"
TextMode="MultiLine"
Width="375px"
/>
</div>
</form>
</body>
</html>
Now that we have the UI, let's go to the code file to see how to implement the asynchronous call. We will leverage the PageAsyncTask type in ASP.NET 2.0, as it makes asynchronous processing so much easier. We will load up a document that has a SOAP envelope to use as a starting point and put that into the first TextBox control. That will give the users something to start with (and help that they don't have to remember the namespace or case-sensitivity for a SOAP Envelope). That file is very simple:
<s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
</s:Header>
<s:Body>
<MyOperation1
xmlns="http://tempuri.org/">
<myValue1>Kirk</myValue1>
</MyOperation1>
</s:Body>
</s:Envelope>
We only want to fill that TextBox if this is not a PostBack. That is, if we didn't check to see if this is a PostBack, then the contents of TextBox1 would be overwritten every page request. By checking PostBack, we limit that the textbox is only filled during the initial page access (the HTTP GET request).
The next step is to actually do something when the user clicks the button. In our UI above, we wired up our UI to our code-behind handler for the button click event by specifying "OnClick="Button2_Click"". That is how the code-behind wires up our handler to the UI. In our button handler, we will new up a custom class called MyAsyncTask (which we will look at in just a moment). That custom class has methods that the PageAsyncTask will call for its begin event, end event, and timeout event. Once we create the PageAsyncTask type, we register it with the Page and then ask for it to be executed. Our ASP.NET Page will not fully complete its pipeline, it will wait for our asynchronous tasks to be completed. However, it will avoid blocking the UI thread and will execute the asynchronous task on a background thread.
using System;
using System.IO;
using System.Web;
public
partial
class
_Default : System.Web.UI.Page
{
protected
void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
//Get the SOAP from somewhere
TextBox1.Text = File.ReadAllText(Server.MapPath("soap.xml"));
}
}
protected
void Button2_Click(object sender, EventArgs e)
{
MyAsyncTask task = new
MyAsyncTask(targetUrl.Text, soapAction.Text, TextBox1.Text);
// create the asynchronous task instance
PageAsyncTask asyncTask = new PageAsyncTask(
new
BeginEventHandler(task.OnBegin),
new
EndEventHandler(task.OnEnd),
new
EndEventHandler(task.OnTimeout),
null);
// register the asynchronous task instance with the page
this.RegisterAsyncTask(asyncTask);
// invoke the asynchronous task
this.ExecuteRegisteredAsyncTasks();
TextBox2.Text = task.SoapResult;
}
}
We set up the UI, and showed how easy it is to call a method that uses the IAsyncResult pattern. It's time to see the work that is being performed asynchronously. We create a custom type, MyAsyncTask, that exposes read-only properties for the URL being accessed, the SOAPAction HTTP header that is used for dispatch on the server side, and the actual XML containing the SOAP envelope that we want to send to the server. We require those bits of information for this class to do its work, so we put them in the constructor for the type.
There are 3 methods for our custom type: OnBegin, OnEnd, and OnTimeout, which map to the 3 events that the PageAsyncTask in ASP.NET 2.0 is looking for in its constructor. The OnBegin method writes XML to the body of the outgoing SOAP request and sets up the HTTP headers. The OnEnd method completes processing by calling the EndGetResponse method and retrieves the result.
using System;
using System.Net;
using System.IO;
using System.Text;
using System.Xml;
public
class
MyAsyncTask
{
private
string _soapResult;
private
string _soapRequest;
private
string _soapAction;
private
string _url;
public MyAsyncTask(string url, string soapAction, string soapRequest)
{
_url = url;
_soapAction = soapAction;
_soapRequest = soapRequest;
}
public
string Url
{
get { return _url; }
}
public
string SoapAction
{
get { return _soapAction; }
}
public
string SoapRequest
{
get { return _soapRequest; }
}
public
string SoapResult
{
get { return _soapResult; }
}
public
IAsyncResult OnBegin(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
WebRequest req = WebRequest.Create(_url);
req.Headers.Add("SOAPAction", SoapAction);
req.ContentType = "text/xml;";
req.Method = WebRequestMethods.Http.Post;
req.UseDefaultCredentials = true;
Stream s = req.GetRequestStream();
XmlWriterSettings settings = new
XmlWriterSettings();
settings.Encoding = System.Text.Encoding.UTF8;
settings.OmitXmlDeclaration = true;
XmlWriter writer = XmlWriter.Create(s,settings );
writer.WriteRaw(_soapRequest);
writer.Flush();
writer.Close();
s.Close();
return req.BeginGetResponse(cb, req);
}
public
void OnEnd(IAsyncResult result)
{
WebRequest request = (WebRequest)result.AsyncState;
WebResponse response = (WebResponse)request.EndGetResponse(result);
Stream s = response.GetResponseStream();
StreamReader reader = new
StreamReader(s);
_soapResult = reader.ReadToEnd();
s.Close();
response.Close();
}
public
void OnTimeout(IAsyncResult ar)
{
_soapResult = "Ansynchronous task failed to complete " +
"because it exceeded the AsyncTimeout parameter.";
}
}
If you want to build a web service to test this out with, just create a new WCF service that will process any XML sent to it and return it back (a poor man's echo, really):
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
[ServiceContract()]
public
interface
IUniversalContract
{
[OperationContract(Action = "*", ReplyAction = "*")]
Message ProcessMessage(Message m);
}
public
class
MyService : IUniversalContract
{
public
Message ProcessMessage(Message m)
{
return m;
}
}
We are just using the "universal contract", which processes any action and message. Configure a basicHttpBinding for the service, and we are good to go:
<?xml
version="1.0"?>
<configuration>
<system.serviceModel>
<services>
<service
name="MyService" >
<endpoint
binding="basicHttpBinding"
contract="IUniversalContract" />
</service>
</services>
</system.serviceModel>
<system.web>
<compilation
debug="true"/>
</system.web>
</configuration>
In our service, we didn't specify a namespace… WCF will use the default namespace "http://tempuri.org/". WCF will dispatch on the back end through the namespace as well as the operation name (same as what is projected through the soap:operation listings in the wsdl:operation section of the service's WSDL document). Since we are using the universal contract for our testing, we could put pretty much anything in this textbox that we want to as long as the service URI. We default in the UI's soapAction TextBox to the following value:
Text="http://tempuri.org/IUniversalContract/MyOperation1"
/>
That's it! Some pretty cool bits of code here. The ASP.NET 2.0 asynchronous processing feature, the System.Net.HttpWebRequest, and even how to process any SOAP (or even POX) using WCF.