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>