Introduction
In this article I will explain how you can consume a web service without using the standard "Add Web Reference" in Visual Studio and how to do this without using SOAP.
"Why do this?" you might ask, well, there are several reasons, for example, you could be discovering the actual web service you wish to consume dynamically and the actual Web Services may vary dramatically, depending on how dynamic you have to be static references to every possible Web Service may not be practical/possible. My reason for doing this: "Out of interest!"
HTTP Background
Firstly, a little background on HTTP or Hyper Text Transfer Protocol, if you've never considered looking in depth at this protocol, that we all so readily use and take for granted, you may be thinking "Oh! No! Too advanced", but you'd be wrong!
HTTP is an incredibly simple protocol, an important thing to note is that HTTP 1.0 is not connection based, a request for a web page or resource from a server is made the response received and then the connection is closed. This poses problems for Web Application creators as there is no innate state in HTTP 1.0 except without the use of things like Cookies. HTTP 1.1 is by default connection based, yet everything still appears to behave like HTTP 1.0.
All HTTP is, is a protocol to send a request and to receive a response. Each request will consist of several headers, an example of which is "Set-Cookie" and another example could be "Content-Length". The former is, essentially, going to have a list of name/value pairs which represent the actual cookies, there is other information such as path, domain and expiry that can be associated with an individual cookie. Requests often contain content too, the header Content-Length is used to explicitly state the length of the content (how surprising!). A response is equally simple and also consists of headers and content an example of the content could be HTML.
I would go into more detail, but I won't as its not the point of this article!
Creating our Generic Web Service Proxy
Note: The namespaces I will use will be:
- System.Text
- System.Text.RegularExpressions
- System.Xml
- System.Net
(you can either fully qualify the classes I use or add the using statements to the top of your code)
Making a HTTP Request and Receiving a HTTP Response
To make a HTTP Request, firstly you need to create an instance of the class HttpWebRequest, to do this you can do the following:
string myURL = http://www.cratos-development.co.uk;
HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(myURL);
Why am I casting the return value of .Create(...)? Well, the .Create() method returns a WebRequest not a HttpWebRequest.
The next step is to set up any header information, there is a header collection you can use, but the most common headers have strongly-typed properties sitting on the HttpWebRequest object. The you should set up any content that is required (I will come to this in the last section).
To receive a response, synchronously, from the web server all you need do is the following:
HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse();
Again, the result of this method is WebResponse and not HttpWebResponse.
This is all done synchronously and by that I mean, when the program reaches the line above, it will not continue executing until a response has been fully received. Although in my Generic Web Service Proxy I have only used the synchronous call, for simplicities sake on the client of this class, in the next section I will briefly cover how to make an Asynchronous call.
Receiving a HTTP Response Asynchronously
To make an Asynchronous request, this is a little more complex, firstly we need to create a method to handle the response when it comes, therefore, your HttpWebRequest object needs to have a larger scope than just one method. Below is a code snippet from the method that will start but not finish the request:
// .. CODE HERE ..
IAsyncResult = this._httpRequest.BeginGetResponse(
new AsyncCallback(this.ResponseReceivedHandler),
null);
// .. CODE HERE ..
AsyncCallback is a delegate that gets fired by the .Net Framework when the full response is received. this.ResponseReceivedHandler is a method that is subscribed to that delegate and therefore, when AsyncCallback is invoked, so too will ResponseReceivedHandler be invoked.
Below is the method (ResponseReceivedHandler) which will get the final response:
private void ResponseReceivedHandler(IAsyncResult result)
{
HttpWebRequest httpRequest = this._httpRequest.EndGetResponse(result);
// .. CODE HERE ..
}
Obtaining Web Service Details
To obtain information about what is needed to call a web method, we can request the web services WSDL, this is obtained via calling the Web Service and passing WSDL as a parameter without a value to the Web Service e.g. http://www.cratos-development.co.uk/cratosdevelopmenttools.asmx?WSDL
This will return an XSD describing the Web Service and all its Web Methods. The following code will obtain the WSDL:
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(this._webServiceURI + "?WSDL");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
System.IO.Stream baseStream = response.GetResponseStream();
System.IO.StreamReader responseStreamReader = new System.IO.StreamReader(baseStream);
string wsdl = responseStreamReader.ReadToEnd();
responseStreamReader.Close();
Calling Web Service Using POST
When making the request to the web service, we need to pass the required parameters, this is done, when using POST without SOAP, by passing to the Web Service a set of name value pairs that look a lot like:
Parameter1=Value1&Parameter2=Value2&Parameter3=Value3
The Generic Web Service Proxy class will expose a method called CallWebMethod, it will take an array of objects ie:
public string CallWebMethod(params object[] parameters)
(this is returning a string for simplicities sake, you could deserialise the result to create an object, but im leaving that up to the client. This is partly due to the fact that many of the Web Methods I work with return Xml (this isn't so good, I know!) )
Using the WSDL and the array of values we will build up a string like the above to place into the content of our HttpWebRequest.
To do this, we will build a string that looks like:
Parameter1=[0]&Parameter2=[1]&Parameter3=[2]
The following four methods obtain the WSDL information and build the above string:
private string GetRequestFormat()
{
const string XPATH_TO_WEB_METHOD_INFORMATION_NODE = "/types/schema/element[@name=\"{0}\"]/*";
const string XPATH_TO_WEB_METHOD_PARAMETERS = "sequence/element";
string xpathToWebMethodInformationNode = string.Format(
PATH_TO_WEB_METHOD_INFORMATION_NODE,
this._webMethodName);
string wsdl = this.GetWSDLForWebMethod();
System.Xml.XmlDocument wsdlDocument = new System.Xml.XmlDocument();
wsdlDocument.LoadXml(wsdl);
System.Xml.XmlNode webMethodInformationNode = wsdlDocument.SelectSingleNode(xpathToWebMethodInformationNode);
System.Xml.XmlNodeList parameterInformationNodes = webMethodInformationNode.SelectNodes(XPATH_TO_WEB_METHOD_PARAMETERS);
return this.BuildRequestFormatFromNodeList(parameterInformationNodes);
}
private string BuildRequestFormatFromNodeList(System.Xml.XmlNodeList parameterInformationNodes)
{
const string PARAMETER_NAME_VALUE_PAIR_FORMAT = "{0}=[{1}]";
System.Text.StringBuilder requestFormatToReturn = new System.Text.StringBuilder();
for(int i = 0; i < parameterInformationNodes.Count; i++)
{
requestFormatToReturn.Append(
string.Format(PARAMETER_NAME_VALUE_PAIR_FORMAT, parameterInformationNodes[i].Attributes["name"].Value,
i) +
((i < parameterInformationNodes.Count - 1 &&
parameterInformationNodes.Count > 1) ? "&" : string.Empty));
}
return requestFormatToReturn.ToString();
}
private string GetWSDLForWebMethod()
{
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(this._webServiceURI + "?WSDL");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
System.IO.Stream baseStream = response.GetResponseStream();
System.IO.StreamReader responseStreamReader = new System.IO.StreamReader(baseStream);
string wsdl = responseStreamReader.ReadToEnd();
responseStreamReader.Close();
return this.ExtractTypesXmlFragment(wsdl);
}
private string ExtractTypesXmlFragment(string wsdl)
{
const string CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_HTTP = "http:";
const string CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_SOAP = "soap:";
const string CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_SOAPENC = "soapenc:";
const string CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_TM = "tm:";
const string CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_S = "s:";
const string CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_MIME = "mime:";
const string CONST_TYPES_REGULAR_EXPRESSION = "<types>[\\s\\n\\r=\"<>a-zA-Z0-9.\\.:/\\w\\d%]+</types>";
System.Collections.ArrayList namespaceDeclarationsToRemove = new System.Collections.ArrayList();
namespaceDeclarationsToRemove.Add( CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_HTTP);
namespaceDeclarationsToRemove.Add( CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_MIME);
namespaceDeclarationsToRemove.Add( CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_S);
namespaceDeclarationsToRemove.Add( CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_SOAP);
namespaceDeclarationsToRemove.Add( CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_SOAPENC);
namespaceDeclarationsToRemove.Add( CONST_XML_NAMESPACE_REFERENCE_TO_REMOVE_TM);
for(int i = 0; i < namespaceDeclarationsToRemove.Count; i++)
{
wsdl = wsdl.Replace((string)namespaceDeclarationsToRemove[i], string.Empty);
}
System.Text.RegularExpressions.Match match =
System.Text.RegularExpressions.Regex.Match(wsdl, CONST_TYPES_REGULAR_EXPRESSION);
return match.Groups[0].Value;
}
(I apologize for using a regular expression to pull out the data, if there any XPath and XML/XSD experts that would like to tell me how to extract the information I require that would be excellent, but until then, this will have to remain as ugly as it is - also it's a pretty ugly regular expression too!)
Ok, so now we are able to generate a single string, woohooo! But, oddly, that was the most complex part of the whole object, in my opinion, what follows now is the method that actually calls the Web Method:
internal string CallWebMethod(params object[] parameters)
{
byte[] requestData =this.CreateHttpRequestData(parameters);
string uri = this._webServiceURI + "/" + this._webMethodName;
HttpWebRequest httpRequest = (HttpWebRequest)HttpWebRequest.Create(uri);
httpRequest.Method = "POST";
httpRequest.KeepAlive = false;
httpRequest.ContentType = "application/x-www-form-urlencoded";
httpRequest.ContentLength = requestData.Length;
httpRequest.Timeout = 30000;
HttpWebResponse httpResponse = null;
string response = string.Empty;
try
{
httpRequest.GetRequestStream().Write(requestData, 0, requestData.Length);
httpResponse = (HttpWebResponse)httpRequest.GetResponse();
System.IO.Stream baseStream = httpResponse.GetResponseStream();
System.IO.StreamReader responseStreamReader = new System.IO.StreamReader(baseStream);
response = responseStreamReader.ReadToEnd();
responseStreamReader.Close();
}
catch(WebException e)
{
const string CONST_ERROR_FORMAT = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Exception><{0}Error>{1}<InnerException>{2}</InnerException></{0}Error></Exception>";
response = string.Format(CONST_ERROR_FORMAT, this._webMethodName, e.ToString(), (e.InnerException != null ? e.InnerException.ToString() : string.Empty));
}
return response;
}
You may notice that there are some member variables, such as this._webServiceURI, these are assign in the constructor as below:
public WebServiceProxy(string webServiceURI, string webMethodName): base()
{
this._webServiceURI = webServiceURI;
this._webMethodName = webMethodName;
string hashTableKey = this._webServiceURI + "/" + this._webMethodName;
if (_webServiceWebMethodRequestFormats.ContainsKey(hashTableKey))
{
this._requestFormat = (string)_webServiceWebMethodRequestFormats[hashTableKey];
}
else
{
this._requestFormat = this.GetRequestFormat();
_webServiceWebMethodRequestFormats.Add(hashTableKey, this._requestFormat);
}
}
In my class I have defined a static member, which is a System.Collections.Hashtable and it is called
_webServiceWebMethodRequestFormats, what this does is to ensure that the WSDL request and interpretation to produce the request format (e.g. Parameter1=[0]&Parameter2=[1]) only happens once.
So, there you have it a Generic Web Service Proxy class, without using SOAP! (I have only given you enough information to enable you to create your own class) Please, if anyone can tell me how to extract the information from the WSDL in a more elegant way, I'd love to know (I only have so much time to play about with XSD's and XPath).