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" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

 

<html xmlns="https://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="https://localhost:49186/WCFService8/Service.svc" />

<br />

<asp:Label runat="server"

ID="Label2"

Text="SOAPAction:" />

<asp:TextBox runat="server"

ID="soapAction"

Width="286px"

Text="https://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="https://schemas.xmlsoap.org/soap/envelope/">

<s:Header>

</s:Header>

<s:Body>

<MyOperation1 xmlns="https://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 "https://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="https://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.