Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Logging SOAP Web Service Envelop (Request and Reply) in .NET

5.00/5 (3 votes)
25 Feb 2015CPOL4 min read 39K   429  
Intercepting WCF and SOAP services for logging SOAP envelope

Introduction

I have been positioned in a project to call an external soap web service. Due to restrictions, direct connection to the host was restricted in the development environment. All I had was a WSDL file.

Restrictions are:

  • Implementation of service on the host is out of reach, no changes could be made
  • Due to the architecture used, the usage of app.config files is not an option. All configuration settings must be done by code at runtime.

Background

With a background in Java development, dealing with web services using service frameworks like Apache CXF and Axis, where logging soap envelop is easily done by the framework, I found it a bit hard to achieve the same result in .NET platform. Several hours of Googling was the cost. So I think the tip will save you some time. Most of the source code in the tip is gathered from CodeProject and Stackoverflow sites, thanks to all.

Using the Code

The story begins with right clicking on my project in VS and adding a service reference, navigating to the wsdl file, the wizard creates the stub magically. Some coding to call the methods and it's done in seconds. So far so good. Release is ready to be launched. After installation and calling the method (in clients data center), an exception of type SoupFaultException is caught with message "An exception occurred!". Now what? What is happening and how to fix the bug?

Let's have a look at our main wsdl file (simplified version) and see what happens under the hood:

XML
<xml version='1.0' encoding='UTF-8'?>
<wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
                  xmlns:tns="samtakws" 
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
                  xmlns:ns2="http://schemas.xmlsoap.org/soap/http"
                  xmlns:ns1="http://webservice/" 
                  name="SamtakWebServiceImplService" targetNamespace="samtakws">
  
  <wsdl:import 
  location="http://someurl.pbn.net/samtak/services/samtakWebService?wsdl=SamtakWebService.wsdl" 
  namespace="http://webservice/">
  </wsdl:import>
  
  <wsdl:binding name="SamtakWebServiceImplServiceSoapBinding" 
  type="ns1:SamtakWebService">
    <soap:binding style="document" 
    transport="http://schemas.xmlsoap.org/soap/http"/>
   <wsdl:operation name="saveAbzarPardakht">
      <soap:operation soapAction="" style="document"/>
      <wsdl:input name="saveAbzarPardakht">
        <soap:body use="literal"/>
      </wsdl:input>
      <wsdl:output name="saveAbzarPardakhtResponse">
        <soap:body use="literal"/>
      </wsdl:output>
      <wsdl:fault name="Exception">
        <soap:fault name="Exception" use="literal"/>
      </wsdl:fault>
    </wsdl:operation>
  </wsdl:binding>
  
  <wsdl:service name="SamtakWebServiceImplService">
    <wsdl:port binding="tns:SamtakWebServiceImplServiceSoapBinding" 
     name="SamtakWebServiceImplPort">
      <soap:address location="http://someurl.pbn.net/samtak/services/samtakWebService"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

The wsdl contains service operations definition, it also points to another wsdl by :import tag, so we will need to fetch the second wsdl from the URL inside import tag. The second wsdl contains operands definition. I am going to call these two wsdl files arz3_samtakWebService.wsdl and arz3_samtakWebService_import.wsdl. As a hint, naming both files with wsdl extension will prevent errors that will be discussed later.

There are two utilities in .NET SDK that can generate proxy of a web service:

  1. svcutil.exe ServiceModel Metadata Utility Tool
  2. wsdl.exe Web Services Description Language Tool

svcutil.exe

In Visual Studio command prompt, navigation to the wsdl files location and running...

svcutil /t:code  /noConfig  *.wsdl  /out:SamtakWebServiceClient.cs

...will generate a WCF client from service metadata in C# language, called SamtakWebServiceClient.cs, with no configuration file. This is the same process when using add service reference wizard is doing behind the scene (but you didn't have the access to use svcutil.exe parameters like /noconfig when using the wizard). To intercept request and reply to log soup envelop, the guid was helpful. Implementing IEndpointBehavior and IClientMessageInspector and extending BehaviorExtensionElement will create an interception ability. To create an instance of the proxy with interception capability and no app.conf file, the snippets below were helpful:

C#
private SamtakWebServiceClient GetWCFLProxy()
{
    var adress = new EndpointAddress
    (new Uri("http://arz3.pbn.net/samtak/services/samtakWebService"));
    
    var binding = new BasicHttpBinding
    {
        CloseTimeout = new TimeSpan(0, 0, 1, 0),
        OpenTimeout = new TimeSpan(0, 0, 1, 0),
        ReceiveTimeout = new TimeSpan(0, 0, 10, 0),
        SendTimeout = new TimeSpan(0, 0, 1, 0),
        AllowCookies = false,
        BypassProxyOnLocal = false,
        HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
        MaxBufferSize = 65536,
        MaxBufferPoolSize = 524288,
        MaxReceivedMessageSize = 65536,
        MessageEncoding = WSMessageEncoding.Text,
        TextEncoding = Encoding.UTF8,
        TransferMode = TransferMode.Buffered,
        UseDefaultWebProxy = true,
        ReaderQuotas =
        {
            MaxDepth = 32,
            MaxStringContentLength = 8192,
            MaxArrayLength = 16384,
            MaxBytesPerRead = 4096,
            MaxNameTableCharCount = 16384
        }
    };
    
    binding.Security.Mode = BasicHttpSecurityMode.None;
    binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
    binding.Security.Transport.ProxyCredentialType = HttpProxyCredentialType.None;
    binding.Security.Transport.Realm = "";
    
    binding.Security.Message.ClientCredentialType = 
                                     BasicHttpMessageCredentialType.UserName;
    binding.Security.Message.AlgorithmSuite = SecurityAlgorithmSuite.Default;
    
    var smtkSrvc = new SamtakWebServiceClient(binding, adress);
    // adding interceptor
    smtkSrvc.Endpoint.Behaviors.Add(new SamtakEndpointBehavior());
    //
    return smtkSrvc;
}

Now logger works like a charm for WCF client.

wsdl.exe

In Visual Studio command prompt, navigation to the wsdl files location and running...

wsdl arz3_samtakWebService.wsdl arz3_samtakWebService_import.wsdl 

...will generate code for XML Web service clients from WSDL contract files in C# language, called SamtakWebServiceImplService.cs, with no configuration file. To intercept request and reply to log soup envelop, this article was used. Extending SoapExtension and SoapExtensionAttribute will create an interception ability. Due to restriction of using app.config files, we need to replace the configuration part of the article:

XML
<configuration>
    <system.web>
       <webServices>
         <soapExtensionTypes>
           <add type="Ideafixxxer.SoapDumper.TraceExtension" priority="1" group="0"/>
         </soapExtensionTypes>
       </webServices>
     </system.web>
</configuration>

The trick below was used, using reflection to write a readonly collection:

C#
[ReflectionPermission(SecurityAction.Demand, Unrestricted = true)]
public SamtakWebServiceImplService GetWSDLProxy()
{
    var soapInterceptor = new SoapExtensionTypeElement
    {
        Type = typeof (SoupLogExtension),
        Priority = 1,
        Group = PriorityGroup.High
    };
    WebServicesSection wss = WebServicesSection.Current;

    // set SoapExtensionTypes collection to read/write...
    var readOnlyField =
       typeof(System.Configuration.ConfigurationElementCollection)
       .GetField("bReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
    readOnlyField.SetValue(wss.SoapExtensionTypes, false);

    // inject SoapExtension...
    wss.SoapExtensionTypes.Add(soapInterceptor);

    // set SoapExtensionTypes collection back to readonly and clear modified flags...
    MethodInfo resetModifiedMethod =
      typeof(System.Configuration.ConfigurationElement)
      .GetMethod("ResetModified", BindingFlags.NonPublic | BindingFlags.Instance);
    resetModifiedMethod.Invoke(wss.SoapExtensionTypes, null);
    MethodInfo setReadOnlyMethod =
      typeof(System.Configuration.ConfigurationElement)
      .GetMethod("SetReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
    setReadOnlyMethod.Invoke(wss.SoapExtensionTypes, null);

    var samtakWebServiceImplService = new SamtakWebServiceImplService
    {
        Url = "http://someUrl.pbn.net/samtak/services/samtakWebService"
    };
    return samtakWebServiceImplService;
}

Now we have the logger.

Comparison of the Soup Envelops

Logger (LogService.cs) is a simple TextFileAppender. The default log file location is "C:\samtalkLog.log".

Calling the web service by WCF client (svcutil.exe) will generate a soup message with the structure below:

XML
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
     <s:Header>
       <Action s:mustUnderstand="1"
       xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none" />
     </s:Header>
     <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <saveAbzarPardakht xmlns="http://webservice/">
         <arg0 xmlns="">
           <bedounPardakhtOrSanad>false</bedounPardakhtOrSanad>
           <shenasehParvandeh>12345</shenasehParvandeh>
         </arg0>
         <arg1 xmlns="">username</arg1>
         <arg2 xmlns="">password</arg2>
       </saveAbzarPardakht>
     </s:Body>
</s:Envelope>

Calling the web service by XML Web service client (wsdl.exe) will generate a soup message with the structure below:

XML
<soap:Envelope xmlns:soap=http://schemas.xmlsoap.org/soap/envelope/
   xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <soap:Body>
           <saveAbzarPardakht xmlns="http://webservice/">
               <arg0 xmlns="">
               <bedounPardakhtOrSanad>false</bedounPardakhtOrSanad>
               <shenasehParvandeh>12345</shenasehParvandeh>
               </arg0>
               <arg1 xmlns="">username</arg1>
               <arg2 xmlns="">password</arg2>
           </saveAbzarPardakht>
       </soap:Body>
</soap:Envelope>

WCF clients have a soup header action mustUnderstand with a Microsoft xmlns uri.

WCF client uses s:Envelope and WSDL client uses soap:Envelope (by XML definition, these are the same).

In our case, WCF request was rejected by the server, causing SoupFaultException. Second request generated by web service client was accepted.

Points of Interest

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)