This article will show you how to trap an exception that occurs within a WCF service but also inform the client application of the exception so it can be handled. To do this we will make use of a custom faultException.
1. You need to create a fault contract for the type of fault that you want to be returned to the client via WCF. Below I have created a ‘DBConcurrencyFault’ fault that tells the client when a database concurrency violation has occurred (e.g. 2 people tried to save the same record).
namespace Common.Services.Faults { [DataContract] public class DBConcurrencyFault { [DataMember] public String Message { get; set; } [DataMember] public String ExceptionType { get; set; } } } |
2. Now we are going to create a new attribute that we can use to decorate our service classes with. This attribute will trap any exception occurring in a WCF service and convert them into a specific fault exception.
namespace Common.Services { public class WcfErrorHandler : Attribute, IErrorHandler, IServiceBehavior { public bool HandleError(Exception error) { return false; } public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault) { if (fault != null) return; if (error.GetType() == typeof(System.Data.DBConcurrencyException)) { // Handle Database record being written to by different users (Conncurrency Fault) DBConcurrencyFault databaseConcurrencyFault = new DBConcurrencyFault(); databaseConcurrencyFault.Message = error.Message; databaseConcurrencyFault.ExceptionType = error.GetType().ToString(); String faultReason = "Someone else has already saved this record."; FaultException<DBConcurrencyFault> faultException = new FaultException<DBConcurrencyFault>(databaseConcurrencyFault, faultReason); MessageFault messageFault = faultException.CreateMessageFault(); fault = Message.CreateMessage(version, messageFault, faultException.Action); } else { // Handle all other errors as standard FaultExceptions FaultException faultException = new FaultException(error.Message); MessageFault messageFault = faultException.CreateMessageFault(); fault = Message.CreateMessage(version, messageFault, faultException.Action); } } #region IServiceBehavior Members public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { foreach (ChannelDispatcher disp in serviceHostBase.ChannelDispatchers) { disp.ErrorHandlers.Add(this); } } public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } #endregion } } |
Note: the ‘ProvideFault’ method contains code that determines if an exception of type ‘System.Data.DBConcurrencyException’ has occurred. If one has it is converted into a ‘DBConcurrencyFault’ which is then returned to the calling client by the WCF service.
3. Now you can either add the WcfErrorHandler attribute to all your wcf services, or you can as I have below define a base service class that all your service classes will inherit from.
namespace Common.Services { [WcfErrorHandler()] public class BaseService { // Common service methods go here } } |
4. Have your service class inherit from the base service class.
public class MileageClaimService : BaseService, IMileageClaimService { // Service methods go here } |
5. Trap the fault exception from a web client and redirect the user to a tailored error page.
5a. For an Asp.Net web application you can add the exception handling code to the global.asax Application_Error method.
protected void Application_Error(object sender, EventArgs e) { Exception ex = Server.GetLastError().GetBaseException(); if (ex.GetType() == typeof(System.ServiceModel.FaultException<Common.Services.Faults.DBConcurrencyFault>) ) { Server.ClearError(); Response.Redirect("~/Errors/ErrorDbConcurrency.aspx"); } } |
5b. From an Asp.Net MVC web application you just add a HandleError attribute to the controller with the type of DBConcurrencyFault and redirect to a specific view.
[HandleError(ExceptionType = typeof(System.ServiceModel.FaultException<Common.Services.Faults.DBConcurrencyFault>), View = "ErrorDbConcurrency")] [HandleError] public class ClaimController : Controller { // Controller methods go here } |
Note: There are 2 HandleError attributes, the first handles the DBConcurrencyFault and redirects to a custom error page. The 2nd traps all other exceptions and redirects to the default error page. The order of these 2 HandleError attributes is important.
Thanks for the information! I couldn't figure out how to get the Server.GetLastError() to work with FaultException. You saved me a lot of time and headaches!
ReplyDelete