In the previous sections, when we were trying to retrieve a product but the product ID passed in was not a valid one, we just threw an exception. Exceptions are technology-specific and, therefore, are not suitable for crossing the service boundary of SOA compliant services. All exceptions generate a fault on the communication channel, resulting in unhappy proxies, as a recover and retry is not possible. Thus, for WCF services, we should not throw normal exceptions.
What we need are SOAP faults that meet industry standards for seamless interoperability.
In the service interface layer, operations that may throw a FaultExceptions
must be decorated with one or more FaultContract
attributes, defining the exact FaultException
.
On the other hand, the service consumer should catch specific FaultExceptions
to be in a position to handle the specified exceptions.
We will now change the exception in the GetProduct
operation to a FaultContract
.
But before we implement our first FaultContract
, we need to modify the App.config
file in the RealNorthwindService
project. We will change the setting includeExceptionDetailInFaults
back to False
, so that every unhandled, non-Fault exception will be a violation. Client applications won't know the details of those exceptions.
You can definitely set includeExceptionDetailInFaults
to True
when debugging, as this will be very helpful in diagnosing problems during the development stage. But in production, it should always be set to False
.
So, open the App.config
file in the RealNorthwindService
project, change includeExceptionDetailInFaults
from True
to False
, and save it.
Next, we will define the FaultContract
. For simplicity, we will define only one FaultContract
, and leave it inside the file IProductService.cs
, although in a real system you can have as many Fault Contracts
as you want, and they should also normally be in their own files.
The FaultContract
will be as follows:
[DataContract] public class ProductFault { public ProductFault(string msg) { FaultMessage = msg; } [DataMember] public string FaultMessage; }
We then decorate the service operation GetProduct
with the following attribute:
[FaultContract(typeof(ProductFault))]
This is to tell the service consumers that this operation may throw a fault of the type ProductFault.
The content of IProductService.cs
should now be:
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace MyWCFServices.RealNorthwindService { // NOTE: If you change the interface name "IService1" here, you must also update the reference to "IService1" in App.config. [ServiceContract] public interface IProductService { [OperationContract] [FaultContract(typeof(ProductFault))] Product GetProduct(int id); [OperationContract] bool UpdateProduct(Product product); // TODO: Add your service operations here } [DataContract] public class Product { [DataMember] public int ProductID; error handling, adding to servicefault contract, adding[DataMember] public string ProductName; [DataMember] public string QuantityPerUnit; [DataMember] public decimal UnitPrice; [DataMember] public bool Discontinued; } [DataContract] public class ProductFault { public ProductFault(string msg) { FaultMessage = msg; } [DataMember] public string FaultMessage; } }
Once we have modified the interface, we need to modify the implementation. Open the ProductService.cs
file, and change the following lines:
if (productEntity == null) throw new Exception("No product found with id " + id);
to these lines:
if (productEntity == null) { //throw new Exception("No product found with id " + id); if (id != 999) throw new FaultException<ProductFault>(new ProductFault( "No product found with id " + id), "Product Fault"); else throw new Exception("Test Exception"); }
This will throw a ProductFault
exception if an invalid ID is passed to the GetProduct
operation. However, we will throw a normal C# exception if the passed ID is 999. Later, we will use this special ID to do an extra test.
Now, build the RealNorthwindService
project. After has been successfully built, we will use the client that we built earlier to test this service. We will examine the channel status after an exception has been thrown. We can't do this with the WCF Service Test Client, because in WCF Test Client, each request will create a new channel, and we don't have a way to examine the channel state after the service call.
Now, let's update the client program, so that the fault exception is handled.
RealNorthwindClient
project, expand the Service References node and right-click on ProductServiceRef. Select Update Service Reference from the context menu, and the Updating Service Reference dialog box will pop up. The WCF Service Host will be started automatically, and the updated metadata information will be downloaded to the client side. Proxy code will be updated with modified and new service contracts. Program.cs
under RealNorthwindClient
project, and add the following method to the class Program:
static void TestException(ProductServiceClient client, int id) error handling, adding to serviceclient program, updating{ Console.WriteLine(" Test {0} Fault Exception for product id {1}...", (id != 999)?"handled":"unhandled", id); try { Product product = client.GetProduct(id); } catch (TimeoutException ex) { Console.WriteLine("The service operation timed out. " + ex.Message); } catch (FaultException<ProductFault> ex) { Console.WriteLine("ProductFault: " + ex.ToString()); } catch (FaultException ex) { Console.WriteLine("Unknown Fault: " + ex.ToString()); } catch (CommunicationException ex) { Console.WriteLine("There was a communication problem. " + ex.Message + ex.StackTrace); } Console.WriteLine(" Channel Status after the exception: " + client.InnerChannel.State.ToString()); Console.WriteLine("Press any key to continue ..."); Console.ReadKey(); }
If 999 is passed to this method as the ID, the service will throw an exception, instead of a fault exception. We will also examine the channel status of this unhandled exception.
Main
in this class:TestException(client, 0); // channel is still open after a FaultException TestException(client, 999); // channel is Faulted after a non handled fault exception Console.WriteLine(" Test Faulted client ..."); product = client.GetProduct(20); // can't use a client with a Faulted channel Console.WriteLine("Press any key to continue ..."); Console.ReadLine();
The full content of the Program.cs
is now as follows:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using RealNorthwindClient.ProductServiceRef; using System.ServiceModel; namespace RealNorthwindClient { class Program { static void Main(string[] args) { ProductServiceClient client = new ProductServiceClient(); Product product = client.GetProduct(23); Console.WriteLine("product name is " + product. ProductName); Console.WriteLine("product price is " + product.UnitPrice. ToString()); product.UnitPrice = (decimal)20.0; bool result = client.UpdateProduct(product); Console.WriteLine("Update result is " + result. ToString()); TestException(client, 0); // channel is still open after a FaultException TestException(client, 999); // channel is Faulted after a non handled fault exception Console.WriteLine(" Test Faulted client ..."); product = client.GetProduct(20); // can't use a client with a Faulted channel Console.WriteLine("Press any key to continue ..."); Console.ReadLine(); } static void TestException(ProductServiceClient client, int id) { Console.WriteLine(" Test {0} Fault Exception for product id {1}...", (id != 999)?"handled":"unhandled", id); try { Product product = client.GetProduct(id); } catch (TimeoutException ex) { Console.WriteLine("The service operation timed out. " + ex.Message); } catch (FaultException<ProductFault> ex) { Console.WriteLine("ProductFault: " + ex.ToString()); } catch (FaultException ex) { Console.WriteLine("Unknown Fault: " + ex.ToString()); } catch (CommunicationException ex) { Console.WriteLine("There was a communication problem. " + ex.Message + ex.StackTrace); } Console.WriteLine(" Channel Status after the exception: " + client.InnerChannel.State.ToString()); Console.WriteLine("Press any key to continue ..."); Console.ReadKey(); } } }
Before we run the program, we need to change a debugging setting. Select menu option Tools | Options, go to the Debugging | General tab, deselect the Enable Just My Code(Managed only) checkbox, as shown in the Options image below:
Enable Just My Code means that while debugging, you look at only the code you have written, and ignore the third-party code that is inside your application (such as the framework and libraries). Just My Code hides non-user code so that it does not appear in the debugger windows. When you step through the code, the debugger steps through any non-user code but does not stop in it. For example, if you call a .NET Framework API that throws an exception, you're going to break in your code that called the API, rather than farther down in the framework. This becomes particularly useful when user and non-user code call back and forth between each other. You can set the My Code status on a per-function level, to specify whether you want certain code debugged.
In our example, if you don't deselect Enable Just My Code, that is, if Just Debug My Code is selected, what happens is that when you debug the client program, you will get an exception popped up in Visual Studio after:
throw new FaultException<ProductFault>(new ProductFault("No product found with id " + id), "Product Fault");
It complains that the exception is not being handled by the user, as seen in the RealNorthwind (Debugging) - Microsoft Visual Studio image below:
Note that this exception window is pointing to the following statement in Visual Studio:
throw new Exception("Test Exception");
But it is not because this line raised an exception. It is because this line is the line after the one that raised the exception, which is the throw new FaultException line. Actually, if you press F5 to continue, the next time the "throw new Exception
" line raises an exception, the popped up window will point to:
Product product = new Product();
We know that the ProductFault
exception will be handled by our client program, but now, Visual Studio thinks that it is not. To avoid this annoyance, you can disable the Just My Code option. However, you should be aware that disabling this option might have some side effects. For example, Visual Studio will not complain if there is a real unhandled exception. You may want to enable it after you have completed your testing.
Once you have changed the Just My Code option, you can press F5 to run the client program (remember to set the RealNorthwindClient
to be the startup project). You will get the output shown in the following screenshot:
As you can see from the output, the client channel to the service is still open, after the ProductFault
is handled in the client program. Next, we will use the same client to get the product details for ID 999.
Press Enter, and more output will be shown, with a fault exception as shown in the image here:
From the output, we know that the channel has now faulted. This means that now the client does not have a valid way to communicate with the service. To prove it, press Enter to try to connect to the service using the same client object, and you will get an unhandled exception "The communication object, System.ServiceModel.Channels.ServiceChannel", cannot be used for communication because it is in the Faulted state", as shown in the RealNorthwind (Debugging) image, below. The program will not continue, so you have to stop it.
In the source code, if we have to call the service again, we have to abort this client, and create a new one for the communication.