Displaying SOAP XML Messages in a Simple WCF Web Service

Introduction

The Windows Communication Foundation WCF is growing in popularity and becoming the mainstream method for creating web services. While WCF can provide a powerful interoperable service, which many different programs and languages can comminute with, it becomes increasingly important to be able to debug the web service’s action at the raw SOAP XML level. Microsoft provides a built-in tool for viewing WCF log files, although it lacks the simplicity of quickly displaying the raw XML sent to and from the WCF service. Luckily, we can utilize the .NET message inspector to add support for our WCF service to output the complete XML SOAP packets that are sent and received.

In this tutorial, we’ll create a basic WCF web service using plain-text username and password authentication. We’ll bypass using SSL and x.509 certificates to keep things simple. We’ll then add a custom attribute to our WCF class to output all incoming and outgoing XML SOAP packet messages to the Visual Studio Console window.

WCF Web Service SOAP XML Messages C# .NET

Creating a Simple WCF Web Service

To start off, we’ll create a basic WCF web service in Visual Studio by choosing the WCF Web Service template to begin a new project. The template will automatically generate a WCF svc class and a service contract interface. To keep things simple, we’ll implement a single method, called SayHelloWorld(), which will accept a composite type of HelloWorldType and return a string. The composite type will allow you to specify a name and a language. The WCF service contract interface appears as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
namespace WcfService1
{
[ServiceContract(Name="HelloWorldService")]
public interface IHelloWorldService
{
[OperationContract]
string SayHelloWorld(HelloWorldType HelloWorldType);
}

[DataContract]
public enum LanguageType
{
[EnumMember]
English,
[EnumMember]
Spanish
};

[DataContract]
public class HelloWorldType
{
LanguageType _language = LanguageType.English;
string _name;

[DataMember]
public LanguageType Language
{
get { return _language; }
set { _language = value; }
}

[DataMember]
public string Name
{
get { return _name; }
set { _name = value; }
}
}
}

As you can see in the above code, our contract is fairly simple. We have a single method for performing SayHelloWorld() and a custom type to help make our SOAP XML a little more interesting. With the interface service contract defined, we can customize the main WCF class as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace WcfService1
{
public class HelloWorldService : IHelloWorldService
{
#region IHelloWorldService Members

public string SayHelloWorld(HelloWorldType HelloWorldType)
{
if (HelloWorldType.Language == LanguageType.English)
{
return "Hello World, " + HelloWorldType.Name + "!";
}
else
{
return "Hola a todos, " + HelloWorldType.Name + "!";
}
}

#endregion
}
}

In the code above, we simply examine the HelloWorldType and return a string based upon the name and language. With the initial code setup, we’ll now implement the plaintext username and password authentication.

PlainText Username and Password Authentication in a WCF Web Service

Normally, Windows expects all WCF services to use a security certificate and encrypted password for authentication. While this is certainly the preferred method for communicating over the un-trusted Internet, it is not always viable, especially in development environments or settings where other forms of security may be used.

WCF Authentication with a Plaintext Password

In our example WCF service, we’ll implement a plaintext username and password validation using a custom binding, named ClearUsernameBinding. We’ll actually configure the ClearUsernameBinding within the web.config for the WCF service (and the WCF client). As with any custom WCF authentication, we’ll need to provide a CustomUserNameValidator, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace WcfService1
{
public class CustomUserNameValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (userName != "username" || password != "password")
{
throw new Exception("Invalid username or password.");
}
}
}
}

In the above code, we have a very basic CustomUserNameValidator, which simply verifies the username and password against hard-coded values. You can easily enhance this to validate against a database or other authentication structures.

To use the plaintext username and password validation, you’ll also need to download and add a reference to ClearUsernameBinding.dll in your project.

With the authentication complete, we can move on to configuring the WCF service’s web.config settings.

WCF Configuration Flexibility Comes With a Price

Often, the trickiest part of developing a WCF web service is configuring the web.config settings. For our example, we’ll be adding a custom binding for the plaintext username and password authentication. We’ll also be adding a custom endpoint and service block, along with a custom behavior for usernameauthentication.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0"?>
<configuration>

<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<extensions>
<bindingExtensions>
<add name="clearUsernameBinding" type="WebServices20.BindingExtenions.ClearUsernameCollectionElement, ClearUsernameBinding" />
</bindingExtensions>
</extensions>

<bindings>
<clearUsernameBinding>
<binding name="myClearUsernameBinding" messageVersion="Soap12" />
</clearUsernameBinding>
</bindings>

<behaviors>
<serviceBehaviors>
<behavior name="HelloWorldServiceBehavior">
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="WcfService1.CustomUserNameValidator, WcfService1" />
</serviceCredentials>
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="HelloWorldServiceBehavior" name="WcfService1.HelloWorldService">
<endpoint address="" binding="clearUsernameBinding" bindingConfiguration="myClearUsernameBinding" contract="WcfService1.IHelloWorldService" />
</service>
</services>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>

</configuration>

The important pieces to the above configuration begin with the binding extension clearUsernameBinding.

1
2
3
4
<bindingExtensions>
<add name="clearUsernameBinding" type="WebServices20.BindingExtenions.ClearUsernameCollectionElement, ClearUsernameBinding" />
</bindingExtensions>

The above block tells our WCF web service to utilize the plaintext username and password authentication, provided by the custom binding reference. Along with the extension, we’ll also need to provide the binding definition, provided by the following block:

1
2
3
4
5
6
<bindings>
<clearUsernameBinding>
<binding name="myClearUsernameBinding" messageVersion="Soap12" />
</clearUsernameBinding>
</bindings>

The remainder of the configuration defines the service behavior, credentials, endpoint, and finally the actual service definition.

Dun Dun Dun Dun Dun .. Inspector WCF!

With our WCF service defined you should be able to run the web service successfully and connect with a client to execute the SayHello() method. We can now move on to adding functionality to output the raw SOAP XML packets sent and received by the WCF service.

Accessing the incoming and outgoing XML SOAP packets can be a bit tricky with WCF, as we need to access a portion of the protocol stack. However, .NET helps ease this by providing the IDispatchMessageInspector interface. Using this feature, we can implement a custom endpoint and behavior extension to pre-process the SOAP message and output it to the console window. We can provide this functionality using the class shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Configuration;

namespace WcfService1
{
/// <summary>
/// SOAP XML worker method to view incoming and /// outgoing SOAP messages to the WCF service.
/// </summary>
public class ConsoleOutputMessageInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
// Make a copy of the SOAP packet for viewing.
MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
Message msgCopy = buffer.CreateMessage();

request = buffer.CreateMessage();

// Get the SOAP XML content.
string strMessage = buffer.CreateMessage().ToString();

// Get the SOAP XML body content.
System.Xml.XmlDictionaryReader xrdr = msgCopy.GetReaderAtBodyContents();
string bodyData = xrdr.ReadOuterXml();

// Replace the body placeholder with the actual SOAP body.
strMessage = strMessage.Replace("... stream ...", bodyData);

// Display the SOAP XML.
System.Diagnostics.Debug.WriteLine("Received:\n" + strMessage);

return null;
}

public void BeforeSendReply(ref Message reply, object correlationState)
{
// Make a copy of the SOAP packet for viewing.
MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
reply = buffer.CreateMessage();

// Display the SOAP XML.
System.Diagnostics.Debug.WriteLine("Sending:\n" + buffer.CreateMessage().ToString());
}
}

/// <summary>
/// SOAP XML Inspector endpoint.
/// </summary>
public class ConsoleOutputBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}

public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
throw new Exception("Behavior not supported on the consumer side!");
}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
ConsoleOutputMessageInspector inspector = new ConsoleOutputMessageInspector();
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
}

public void Validate(ServiceEndpoint endpoint)
{
}
}

public class ConsoleOutputBehaviorExtensionElement : BehaviorExtensionElement
{
protected override object CreateBehavior()
{
return new ConsoleOutputBehavior();
}

public override Type BehaviorType
{
get
{
return typeof(ConsoleOutputBehavior);
}
}
}

/// <summary>
/// SOAP XML Inspector attribute.
/// Add the attribute to your WCF class definition, as follows:
/// [ConsoleHeaderOutputBehavior]
/// public class MyWCFService : IMyWCFService { ... }
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ConsoleHeaderOutputBehavior : Attribute, IServiceBehavior
{
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
}

public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
for (int i = 0; i < serviceHostBase.ChannelDispatchers.Count; i++)
{
ChannelDispatcher channelDispatcher = serviceHostBase.ChannelDispatchers[i] as ChannelDispatcher;
if (channelDispatcher != null)
{
foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
{
ConsoleOutputMessageInspector inspector = new ConsoleOutputMessageInspector();
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
}
}
}
}

public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
}
}

The first item to note above is the ConsoleHeaderOutputBehavior class, which inherits from the Attribute class. While this class implements the IServiceBehavior interface, to act as a service behavior in the web.config for the WCF web service, it also functions as a class attribute. This allows us to decorate our main WCF service class to provide the SOAP message output functionality.

The meat of the Inspector class is the BeforeSendReply() and AfterReceiveRequest() methods. These two methods pre-process and post-process the SOAP XML packets, allowing us to intercept and peek at the raw content. We can then output the resulting XML, in its entirety, to the Visual Studio Console window, a text file, or any other output method.

Note in the above code, we first make a copy of the MessageBuffer processed by the WCF service. The MessageBuffer contains the SOAP XML, including the header and body of the request or response. However, the body XML content of the SOAP message is further encoded. It’s also important to note that if we were to access the original MessageBuffer’s body content (without first making a copy), the body would be marked as processed and would no longer continue through the web service. Due to this, we first copy the MessageBuffer via CreateBufferedCopy() and then access the message. The message will contain a placeholder text of “… stream …” where the encoded body text is placed. To access the body of the SOAP XML, we call GetReaderAtBodyContents() and then read the outer XML, replacing it back into our output string. We now have the complete SOAP XML request and response for displaying.

The Inspector Needs Some Web.Config Too

With the Inspector class defined, we just need to add it to the WCF web service’s call stack as a custom endpoint behavior. We can do this by adding the following sections to our web.config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    <extensions>

<!-- Add the inspector attribute as a behavior for displaying SOAP XML packets -->
<behaviorExtensions>
<add name="consoleOutputBehavior" type="WcfService1.ConsoleOutputBehaviorExtensionElement, WcfService1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>

...

<behaviors>
<!-- Add the inspector behavior -->
<endpointBehaviors>
<behavior name="inspectorBehavior">
<consoleOutputBehavior />
</behavior>
</endpointBehaviors>

...

With the web.config complete, we can finally add our new WCF Message Inspector to our main WCF class, as follows:

1
2
3
4
5
6
7
8
9
namespace WcfService1
{
[ConsoleHeaderOutputBehavior]
public class HelloWorldService : IHelloWorldService
{
...
}
}

The above attribute tells WCF to process the custom SOAP message inspector, which will display all incoming and outgoing SOAP messages.

Testing with a WCF Client

A basic client can be developed in Visual Studio by creating a Console Application and adding a Service Reference pointing to the above WCF web service. Since the WCF service uses username authentication, you’ll need to provide a username and password. Upon executing any of the WCF service’s methods, you’ll see the raw SOAP XML packets output to the console window of the service.

To setup the test, first start the WCF service so that it is running. If you are in Debug mode, you can simply close the WCF Test tool to leave the service running in the background. Next, in Visual Studio for the WCF service, click Tools->Attach to Process and choose WebDev.WebServer.EXE (or w3p.exe etc) which is hosting the WCF service. Once attached, you’ll be able to view the console window output.

The WCF client code would appear as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
ServiceReference1.HelloWorldServiceClient client = new ServiceReference1.HelloWorldServiceClient();

client.ClientCredentials.UserName.UserName = "username";
client.ClientCredentials.UserName.Password = "password";

ServiceReference1.HelloWorldType helloWorldType = new ServiceReference1.HelloWorldType();
helloWorldType.Name = "Jane Doe";
helloWorldType.Language = ServiceReference1.LanguageType.English;

Console.WriteLine(client.SayHelloWorld(helloWorldType));
Console.ReadKey();
}

Notice we instantiate the HelloWorldServiceClient and populate the ClientCredentials with a plaintext username and password. We can then execute the SayHelloWorld() method, which will allow us to view the incoming and outgoing SOAP messages in the WCF service debugger.

Note the WCF client will also need a reference to the ClearUsernameBinding.dll and appropriate app.config settings, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<extensions>
<bindingExtensions>
<add name="clearUsernameBinding" type="WebServices20.BindingExtenions.ClearUsernameCollectionElement, ClearUsernameBinding" />
</bindingExtensions>
</extensions>
<bindings>
<clearUsernameBinding>
<binding name="myClearUsernameBinding" messageVersion="Soap12" />
</clearUsernameBinding>
</bindings>
<client>
<endpoint address="http://localhost:14946/HelloWorldService.svc"
binding="clearUsernameBinding" bindingConfiguration="myClearUsernameBinding"
contract="ServiceReference1.HelloWorldService" name="myClearUsernameBinding" />
</client>
</system.serviceModel>
</configuration>

WCF Client Output:

Hello World, Jane Doe!

WCF Service Output:

Received:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<s:Envelope xmlns:s=http://www.w3.org/2003/05/soap-envelope xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<u:Timestamp u:Id="_0">
<u:Created>2010-07-08T18:55:27.640Z</u:Created>
<u:Expires>2010-07-08T19:00:27.640Z</u:Expires>
</u:Timestamp>
<o:UsernameToken u:Id="uuid-a166c9cf-7951-4a6a-a35d-1366912188fa-1">
<o:Username>username</o:Username>
<o:Password>password</o:Password>
</o:UsernameToken>
</o:Security>
<To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:14946/HelloWorldService.svc</To>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/HelloWorldService/SayHelloWorld</Action>
</s:Header>
<s:Body><SayHelloWorld xmlns="http://tempuri.org/"><HelloWorldType xmlns:a=http://schemas.datacontract.org/2004/07/WcfService1 xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><a:Language>English</a:Language><a:Name>Jane Doe</a:Name></HelloWorldType></SayHelloWorld></s:Body>
</s:Envelope>

Sending:

1
2
3
4
5
6
7
8
9
10
11
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/HelloWorldService/SayHelloWorldResponse</Action>
</s:Header>
<s:Body>
<SayHelloWorldResponse xmlns="http://tempuri.org/">
<SayHelloWorldResult>Hello World, Jane Doe!</SayHelloWorldResult>
</SayHelloWorldResponse>
</s:Body>
</s:Envelope>

You can download the source code for this project here.

Conclusion

WCF is a powerful technology for creating interoperable web services. With the growing trend in inter-communication amongst applications, WCF is likely to continue growing and allow an even greater number of programs written in many different programming languages to communicate with one another. While configuration settings and debugging for WCF web services can become complicated, viewing the actual SOAP XML messages received and sent from the WCF service can help boost productivity and track down issues. By implementing a custom WCF SOAP message inspector, we can monitor SOAP messages from within the Visual Studio debug environment and help optimize the creation of future WCF web services.

About the Author

This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.

Share