I was engaged recently to design and write a WCF service that is to be consumed by an ASP.NET 2.0 client. While I was aware that with a WCF service and a WCF client, using a System.ServiceModel.FaultException<TDetail> was ideal for handling exception conditions, I wasn’t sure how this was going to be handled by .NET 2.0 since FaultException<TDetail> was introduced in .NET 3.0 – in .NET. 2.0, a SOAP faults are caught with a System.Web.Services.Protocols.SoapException.
So off to trial and error I went and here is what I proposed.
Firstly, it is important to write a WCF service that can be consumed by a WCF client as well as a non-WCF client. To this end, throwing SOAP faults should be done in a manner that both WCF clients and non-WCF clients can understand. My first step was to make sure I had a standard WCF service with a WCF client that could understand the SOAP faults. I created a DataContract for the SOAP fault I wanted to throw when certain conditions were met:
///<summary>
/// Thrown when no matching MPI is found.
/// </summary>
[DataContract(Namespace = WcfConfiguration.XmlNamespace)]
public class MpiNotFoundFault
{
private string _mpi;
///<summary>
/// Gets or sets the MPI.
///</summary>
[DataMember]
public string Mpi
{
get { return _mpi; }
set { _mpi = value; }
}
}
I then specified my FaultContract on the service interface definition as follows:
[ServiceContract]
public interface IMyPinService
{
[OperationContract]
[FaultContract(typeof(MpiNotFoundFault), Name ="MpiNotFoundFault", Namespace = WcfConfiguration.XmlNamespace, Action = WcfConfiguration.XmlNamespace +"/MpiNotFoundFault")]
bool RequestPin(string mpi, DateTime dob);
}
So far, so good. Now, let’s see how we might throw this SOAP fault in the service implementation. For the WCF client to work, I threw the exception as follows:
public bool RequestPin(string mpi, DateTime dob)
{
// Code elided
throw new FaultException<MpiNotFoundFault>(new MpiNotFoundFault() { Mpi = mpi });
}
Then, in the WCF client, all I have to do is this:
// With a WCF client, we can catch specific exceptions and get to each fault contract"s
// contents by accessing the ex.Details propery.
catch (FaultException<MpiNotFoundFault> ex)
{
Console.WriteLine(“MPI {0} was not found.“, ex.Detail.Mpi);
}
Yes, WCF does a lot of work for us. However, when your client is not WCF, SOAP faults get a little bit more complicated. Fortunately, there is a SOAP specification that can be referred to. The SOAP 1.2 specification on soap faults is located here: http://www.w3.org/TR/soap12-part1/#soapfault. Notice I am introducing you to SOAP 1.2, not SOAP 1.1. Yet, in the .NET 2.0 framework, when you use wsdl.exe to create a proxy class, the SOAP 1.1 protocol is defaulted. That’s somewhat of a problem because is SOAP 1.1, faults may only have a faultcode and a faultstring. It wasn’t until SOAP 1.2 that hierarchical codes and subcodes were defined. To see the difference between the two, see http://hadleynet.org/marc/whatsnew.html#S3.1.4.
Why is this important you ask? Well, if in my ASP.NET client (that does not know anything about WCF) all I have is a SoapException, then I’d like to use the code and subcode of the faults to communicate detail about the exception (of course, my service consumer is a trusted source and will filter the information appropriately before passing it on to its users, but that’s another topic entirely).
Here is what I want to do. In my ASP.NET client, I want to check the code and subcode to understand what the nature of the problem was. Here is what my ASP.NET catch statement looks like this:
// In WCF we can catch specific exceptions by using FaultException<T> where T is one of the fault contracts
// included in the service operation. In contrast, ASP.NET web services only allow for catching SoapException.
// To get to the detail, use the SOAP Fault Code and Subcode as shown below.
catch (SoapException ex)
{
// The SOAP fault code always contains the string ‘CredentialValidationRequestFailed’
Debug.WriteLine(ex.Code.Name); // prints the string "CredentialValidationRequestFailed"
// The SOAP fault subcode contains the actual soap fault name without the Fault suffix
Debug.WriteLine(ex.SubCode.Code.Name); // prints the string "MpiNotFound"
}
By doing this, I can see that my call failed because of a CredentialValidationRequestFailed code. This code could occur for any number of reasons. In this example, it occurred because of an MpiNotFound subcode, but I have another 3 subcodes that could have been the cause of the fault.
So what do I need to do to achieve this? Well I need to use SOAP 1.2 for sure. On the service side, I configure the service using an HttpBinding. By default, this binding uses SOAP 1.1. and it can’t be changed. To use SOAP 1.2 for message encoding, I create a custom binding. My configuration file now looks like this:
<system.serviceModel>
<bindings>
<customBinding>
<binding name="basicHttpSoap12Binding">
<textMessageEncoding messageVersion="Soap12"/>
<httpTransport/>
</binding>
</customBinding>
</bindings>
<services>
<service name="MySoap12Service">
<endpoint address="" binding="customBinding" bindingConfiguration="basicHttpSoap12Binding"
bindingNamespace="MySoap12ServiceNamespace"
contract="MySoap12Service">
</endpoint>
</service>
</services>
</system.serviceModel>
On the client side, I use wsdl.exe to generate the proxy class; instead of letting the default SOAP 1.1 protocol apply, I pass in the protocol switch to specify the SOAP 1.2 protocol should be used as follows: wsdl.exe /protocol:SOAP12. This causes the generated proxy to specify the SOAP 1.2 protocol in its constructor:
public partial class MyService : System.Web.Services.Protocols.SoapHttpClientProtocol {
// code elided
public MyService() {
this.SoapVersion = System.Web.Services.Protocols.SoapProtocolVersion.Soap12;
// code elided
}
// code elided
Then, I need to throw my SOAP faults with a bit more information. Here is the revised code snippet for throwing a SOAP fault that can now be understood by non-WCF clients:
throw new FaultException<MpiNotFoundFault>(new MpiNotFoundFault() { Mpi = mpi },
String.Format(CultureInfo.InvariantCulture, “MPI ‘{0}’ not found.“, mpi),
new FaultCode(“CredentialValidationRequestFailed“, new FaultCode(“MpiNotFound“, WcfConfiguration.XmlNamespace)));
That’s it. I throw the FaultException<MpiNotFoundFault> exception, passing in a new MpiNotFoundFault object into the constructor, then I pass in the reason as the “MPI ‘xyz’ not found” string, and then I create my code and subcodes. And voila, I have happy WCF clients and happy non-WCF clients.
On the wire, the SOAP fault looks like this:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header />
<s:Body>
<s:Fault>
<s:Code>
<s:Value>s:CredentialValidationRequestFailed</s:Value>
<s:Subcode>
<s:Value xmlns:a="MyNamespace">a:MpiNotFound</s:Value>
</s:Subcode>
</s:Code>
<s:Reason>
<s:Text xml:lang="en-US">MPI '123123' not found.</s:Text>
</s:Reason>
<s:Detail>
<MpiNotFoundFault xmlns="MyNamespace" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Mpi>123123</Mpi>
</MpiNotFoundFault>
</s:Detail>
</s:Fault>
</s:Body>
</s:Envelope>