Writing custom requests to simple WCF services
Quite often one needs to talk to a WCF service, but using a (WCF) proxy is not a viable alternative. Sometimes the language used isn't a .NET one, the client might not have the .NET framework installed, or the overhead of the proxy is too big for the application need. In this case, creating a request "from scratch" (i.e., using sockets or HTTP requests directly) is usually the best option. In this case, the developer needs to find out the format of the message that is to be sent to the service.
The "correct" way of finding out this format is to look at the metadata from the service, and create a request that complies with all the assertions on the WSDL (or MEX policies). This is usually overkill for simple applications, as a simple template-based input would suffice. This post will present some ways of looking at the message sent from a WCF client (even though you'd not use one in production) and finding out this template.
1. Network capture (the easiest way)
If you can look at what is going on over the wire, you'll be able to see the message. I've found that Fiddler (https://www.fiddler2.com) is one of the best "men-in-the-middle" tools for this kind of task. It installs itself as a proxy in the machine, and any request that goes to https://<machine_name>... will be intercepted by it (notice that for it to work you can't use localhost, unless you configure the proxy settings to not bypass it). It has some problems on Vista, so sometimes it may not be feasible to use it. Also, if the transport of the message is not HTTP, this will not work either.
If you can split the client and server in two machines, then Netmon (https://www.microsoft.com/downloads/details.aspx?familyid=18b1d59d-f4d8-4213-8d17-2f6dde7d7aac&displaylang=en) will work all the times. This is also the best solution if you use TCP to talk to the service.
Network capture is the best way to deal with non-XML messages, as the next option will only show the XML representation of the message, even if it's encoded in some other form (such as JSON, MTOM or the binary encoding).
2. Message Logging (the WCF-only way)
WCF provides a way to log the incoming/outgoing messages, and this will allow you to see what the server is receiving. Enabling message logging with the SvcConfigEditor (https://msdn2.microsoft.com/en-us/library/ms732009.aspx, installed with the SDK) tool is very simple (select "Diagnostics", then "Enable Message Logging"), although I also like to add the "logEntireMessage='true'" option when trying to inspect messages. This config file below enables logging of all messages that flow through the service. The best way to look at the messages is with the SvcTraceViewer tool (https://msdn2.microsoft.com/en-us/library/ms732023.aspx, also installed with the SDK). If the trace viewer isn't available, I find it a quick solution to open the file with notepad, put a "<root>" in the beginning of it, a "</root>" at the end of it, and then opened it on IE (the message log file is an XML file, but it doesn't contain a root element).
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.diagnostics>
<sources>
<source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing">
<listeners>
<add name="ServiceModelMessageLoggingListener">
<filter type="" />
</add>
</listeners>
</source>
</sources>
<sharedListeners>
<add initializeData="messages.svclog"
type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="ServiceModelMessageLoggingListener" traceOutputOptions="Timestamp">
<filter type="" />
</add>
</sharedListeners>
</system.diagnostics>
<system.serviceModel>
<diagnostics>
<messageLogging logMessagesAtTransportLevel="true" logEntireMessage="true"/>
</diagnostics>
</system.serviceModel>
</configuration>
3. Message interception (harder way)
This is definitely overkill for this problem (finding out the messages themselves), so I'll leave their details to a future post. There are two alternatives here. One "less hard": add an instance of an IDispatchMessageInspector (https://msdn2.microsoft.com/en-us/library/system.servicemodel.dispatcher.idispatchmessageinspector.aspx) to the list of the inspectors on the endpoint (you'll need an endpoint behavior to do that). The other (harder) is the one that involves more work, which involves creating a custom MessageEncoder, which can look at the incoming/outgoing message bytes as they are on the wire, and then add all the plumbing required to add it to the binding (a MessageEncoderFactory and a MessageEncodingBindingElement). This custom message encoder would simply delegate the work to the "actual" encoder that is being used by the service.
Examples
Below is a service contract and some examples of requests to each of its three operations. The parts in bold in the requests are to be replaced by the actual values in the request.
[
DataContract]
public class MyDC
{
[DataMember]
public string str = "The string";
}
[ServiceContract]
public interface ITest
{
[OperationContract]
string EchoString(string text);
[OperationContract]
int Add(int x, int y);
[OperationContract]
MyDC EchoDC(MyDC input);
}
public class Service : ITest
{
public string EchoString(string text)
{
return text;
}
public int Add(int x, int y)
{
return x + y;
}
public MyDC EchoDC(MyDC input)
{
return input;
}
}
static Binding GetBinding()
{
BasicHttpBinding result = new BasicHttpBinding();
return result;
}
public static void Test()
{
string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
host.AddServiceEndpoint(typeof(ITest), GetBinding(), "");
host.Open();
Console.WriteLine("Host opened");
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(GetBinding(), new EndpointAddress(baseAddress));
ITest proxy = factory.CreateChannel();
Console.WriteLine(proxy.EchoString("Hello"));
Console.WriteLine(proxy.EchoDC(new MyDC()));
Console.WriteLine(proxy.Add(3, 5));
((IClientChannel)proxy).Close();
factory.Close();
Console.Write("Press ENTER to close the host");
Console.ReadLine();
host.Close();
}
EchoDC:
POST /Service HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "https://tempuri.org/ITest/EchoDC"
Host: THE_MACHINE_NAME:8000
Content-Length: THE_ACTUAL_LENGTH
<s:Envelope xmlns:s="https://schemas.xmlsoap.org/soap/envelope/"><s:Body><EchoDC xmlns="https://tempuri.org/"><input xmlns:a="https://schemas.datacontract.org/2004/07/WCFForums" xmlns:i="https://www.w3.org/2001/XMLSchema-instance"><a:str>The string</a:str></input></EchoDC></s:Body></s:Envelope>
Add:
POST /Service HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "https://tempuri.org/ITest/Add"
Host: THE_MACHINE_NAME:8000
Content-Length: THE_ACTUAL_LENGTH
<s:Envelope xmlns:s="https://schemas.xmlsoap.org/soap/envelope/"><s:Body><Add xmlns="https://tempuri.org/"><x>3</x><y>5</y></Add></s:Body></s:Envelope>
EchoString:
POST /Service HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "https://tempuri.org/ITest/EchoString"
Host: THE_MACHINE_NAME:8000
Content-Length: THE_ACTUAL_LENGTH
<s:Envelope xmlns:s="https://schemas.xmlsoap.org/soap/envelope/"><s:Body><EchoString xmlns="https://tempuri.org/"><text>Hello</text></EchoString></s:Body></s:Envelope>
Comments
- Anonymous
February 16, 2009
Great post carlos, Just a little thing that i needed to ask.I have gotten myself into a situation that requires me to use a socket based client to communicate with a WCF service that is hosted on net.tcp protocol.How can I manage to make this communication possible?I have tried to use TcpClient to send a simple text as request but I got the exception: "The existing connection was forcibly closed by remote host".Then i tried to use a WCF only client and it succeeded (as expected). So, I got into IDispatchMessageInspector and managed to trace the message that the WCF client sent.After getting the message, I sent the exact same message via socket and the dreadful exception came back to haunt me.Can you suggest some solution for this case? Any hint, any existing article?Thanks again for the great post. - Anonymous
February 23, 2009
One thing you can try is to use first a HttpWebRequest (a little lower level than WCF client, a little higher than sockets) to get that working, then move down the level as that goes well. The example above, using a custom HTTP request, is shown below (I just tried building and running it, it worked :)using System;using System.IO;using System.Net;using System.Runtime.Serialization;using System.ServiceModel;using System.ServiceModel.Channels;using System.Text;[DataContract(Namespace="http://my.data.contract.namespace")]public class MyDC{ [DataMember] public string str = "The string";}[ServiceContract]public interface ITest{ [OperationContract] string EchoString(string text); [OperationContract] int Add(int x, int y); [OperationContract] MyDC EchoDC(MyDC input);}public class Service : ITest{ public string EchoString(string text) { return text; } public int Add(int x, int y) { return x + y; } public MyDC EchoDC(MyDC input) { return input; }}public class A{ static Binding GetBinding() { BasicHttpBinding result = new BasicHttpBinding(); return result; } public static void Main() { string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)); host.AddServiceEndpoint(typeof(ITest), GetBinding(), ""); host.Open(); Console.WriteLine("Host opened"); HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress); req.Method = "POST"; req.ContentType = "text/xml; charset=utf-8"; req.Headers["SOAPAction"] = "http://tempuri.org/ITest/EchoDC"; string reqBody = @"<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/""> <s:Body> <EchoDC xmlns=""http://tempuri.org/""> <input xmlns:a=""http://my.data.contract.namespace"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""> <a:str>The string</a:str> </input> </EchoDC> </s:Body></s:Envelope>"; byte[] reqBodyBytes = Encoding.UTF8.GetBytes(reqBody); req.GetRequestStream().Write(reqBodyBytes, 0, reqBodyBytes.Length); req.GetRequestStream().Close(); HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription); if (resp.ContentLength > 0) { Console.WriteLine(new StreamReader(resp.GetResponseStream()).ReadToEnd()); } resp.Close(); Console.Write("Press ENTER to close the host"); Console.ReadLine(); host.Close(); }} - Anonymous
June 10, 2013
Hi, Nice one Carlos. I am using SOAP message for requesting the server. But I am ending in Page not found ERROR. so please help us how to make a proper SOAP requestThank you.Manikanta - Anonymous
June 10, 2013
Try capturing the networking request sent from a tool such as the WCFTestClient. Then compare it with the request you're sending - that should tell you what you need to change. - Anonymous
July 18, 2013
I am getting an exception "The remote server returned an error: (500) Internal Server Error." at (HttpWebResponse)req.GetResponse(). I am getting proper response when tried from SOAPUI. The application uses a basicHttpBinding. Am I missing anything - Anonymous
July 19, 2013
RajeshBabu, you should try comparing the request sent via SOAP UI and the request you're sending programmatically, preferably in a networking capture tool such as Fiddler. If the two requests are exactly the same (including HTTP headers - the BasicHttpBinding uses the "SOAPAction" header), then the request should succeed. Another thing to try is to enable tracing at the server side, to see what is causing the request to be rejected. - Anonymous
July 20, 2013
Hi Carlos,Fiddler really helped. You were correct. The SOAPAction that I was adding was different. Thanks a lot. This is a fine post.Regards,Rajesh Babu M