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 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:
- svcutil.exe ServiceModel Metadata Utility Tool
- 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:
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);
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:
<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:
[ReflectionPermission(SecurityAction.Demand, Unrestricted = true)]
public SamtakWebServiceImplService GetWSDLProxy()
{
var soapInterceptor = new SoapExtensionTypeElement
{
Type = typeof (SoupLogExtension),
Priority = 1,
Group = PriorityGroup.High
};
WebServicesSection wss = WebServicesSection.Current;
var readOnlyField =
typeof(System.Configuration.ConfigurationElementCollection)
.GetField("bReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
readOnlyField.SetValue(wss.SoapExtensionTypes, false);
wss.SoapExtensionTypes.Add(soapInterceptor);
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:
<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:
<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