Michele Leroux Bustamante suggested three approaches for defining faults in WCF at Dev Connections this week:
- Expose the CLR exceptions directly by declaring them as fault contracts in the service’s interface definition.
- Define a data contract for each type of fault that can occur and declare when each may be raised by associating them with the operations that may throw them.
- Create a couple of data contracts, ReceiverFault and SenderFault, with members that indicate the actual error condition and associate them with every operation.
When we first started using WCF, we used approach one. This was simple and allowed us to cope with exceptions the same way that we always had in .NET; however, it wasn’t long before we ran into problems. Usually, authors, speakers, and bloggers discourage this approach because it can expose sensitive or technology-specific information beyond a service’s boundary, a practice that makes a system non-service-oriented. This is reason enough, but I’ll provide another in case you still aren’t persuaded.
Our Visual Studio solutions were all laid out the same way: we had a project for contracts, data access, service type (i.e., service implementation), a host, and our unit tests. The latter includes a reference to the data access project and the host project, so that we could write tests that exercised the data access library and the service. When adding the service reference in the test project, we didn’t want to reuse the types in referenced assemblies because this would cause our test harness to use different data types than those used by our eventual clients. For this reason, in the Service Reference Settings dialog box of Visual Studio 2008 beta 2, we unchecked “Reuse types in referenced assemblies” (as seen below).
After doing so, updating the service reference generated code that did not include the proxy class! After a lot of head scratching, we noticed that the output window had some warnings that looked rather alarming:
Custom tool warning: Cannot import wsdl:portType
Detail: An exception was thrown while running a WSDL import extension: System.ServiceModel.Description.DataContractSerializerMessageContractImporter
Error: ISerializable type with data contract name 'MyException' in namespace 'http://schemas.datacontract.org/2004/07/MyCompany.MyProject.Common' cannot be imported. The data contract namespace cannot be customized for ISerializable types and the generated namespace 'MyCompany.MyProject.MyService' does not match the required CLR namespace 'MyCompany.MyProject.Common'. Check if the required namespace has been mapped to a different data contract namespace and consider mapping it explicitly using the namespaces collection.
What on Earth does that mean and why wasn’t it an error? After some digging, we found that we had to edit the SVCMAP file of the client (seen under the service reference when all files in the Solution Explorer are visible) to map the XML namespace to the CLR namespace as the error message describes. We defined this mapping like this:
TargetNamespace="http://schemas.datacontract.org/2004/07/MyCompany.MyProject.Common" ClrNamespace="MyCompany.MyProject.Common" />
<NamespaceMapping TargetNamespace="http://schemas.datacontract.org/2004/07/MyCompany.MyProject.MyService.Contracts" ClrNamespace="MyCompany.MyProject.MyService.Contracts" />
After updating the SVCMAP file as above, the proxy class was always generated. What on Earth? Why? I’m afraid I don’t know. But, I do know that every client had to know to make this change and then had to do it before they could use our exceptions that were being defined directly as fault contracts.
Because of this, we had two other choices: Bustamante’s second or third fault handling approaches. The second one would inevitably be a lot of code. Each exceptional condition that could be raised by a downstream library or service would need a fault contract. Each would have a description, error code, etc. Considering that we already had a such a hierarchy deriving from System.Exception that was used to map error codes raised in our stored procedures (triggered by calls to RAISERROR), we didn’t want a second hierarchy for faults. Because of this, we adopted Bustamante’s third approach. Rather than explain the specifics of this approach, I suggest you go straight to the source. Watch Bustamante’s Web cast on fault handling. Specifically, check out the material around time codes 24:00 and 49:00.