We are using the excellent ELMAH to deal with unhandled exceptions in an ASP.NET 3.5 web application. This works extremely well for all of the site apart from WCF services which are being consumed using the REST features. When an exception occurs within the operation methods that is not handled by the application code, WCF handles it in various ways depending on the service contracts and configuration settings. This means that the exception does not end up firing the ASP.NET HttpApplication.Error event that ELMAH uses. The two solutions I am aware of to deal with this are:
- Wrap all method calls in a try { } catch(Exception ex) { Elmah.ErrorSignal.FromCurrentContext().Raise(ex); throw; } to explicitly call Elmah within the catch block.
- Use IErrorHandler as described in Will Hughes' blog post Making WCF and ELMAH play nice together to factor out the call to ELMAH to a separate ErrorHandler.
The first option is extremely simple but is not exactly DRY. The second option only requires you to decorate each service with the custom attribute after implementing the attribute and the ErrorHandler. I have done this based on Will's work but I want to verify that this is the correct approach before posting the code.
Is there a better way that I have missed?
The MSDN documenation for IErrorHandler says that the HandleError method is the place to do the logging but ELMAH accesses the HttpContext.Current.ApplicationInstance, which is null within this method even though HttpContext.Current is available. Making the call to Elmah within the ProvideFault method is a workaround as ApplicationInstance is set but this does not match the intent described in the API documentation. Am I missing something here? The documentation does state that you should not rely on the HandleError method being called on the operation thread which may be why ApplicationInstance is null in this scope.
The solution from my blog post (referenced in the OP) was based on an existing solution we were/are using to alter HTTP Response Codes during an error state.
So, for us it was a one-line change to pass the Exception to ELMAH. If there's a better solution, I'd love to know about it too.
For Posterity/Reference, and potential improvement - here's the code from the current solution.
HttpErrorHandler and ServiceErrorBehaviourAttribute Classes
using System;
using System.ServiceModel;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Collections.ObjectModel;
using System.Net;
using System.Web;
using Elmah;
namespace YourApplication
{
/// <summary>
/// Your handler to actually tell ELMAH about the problem.
/// </summary>
public class HttpErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return false;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error != null ) // Notify ELMAH of the exception.
{
if (System.Web.HttpContext.Current == null)
return;
Elmah.ErrorSignal.FromCurrentContext().Raise(error);
}
}
}
/// <summary>
/// So we can decorate Services with the [ServiceErrorBehaviour(typeof(HttpErrorHandler))]
/// ...and errors reported to ELMAH
/// </summary>
public class ServiceErrorBehaviourAttribute : Attribute, IServiceBehavior
{
Type errorHandlerType;
public ServiceErrorBehaviourAttribute(Type errorHandlerType)
{
this.errorHandlerType = errorHandlerType;
}
public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
{
}
public void AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler;
errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;
channelDispatcher.ErrorHandlers.Add(errorHandler);
}
}
}
}
Usage Example
Decorate your WCF Services with the ServiceErrorBehaviour Attribute:
[ServiceContract(Namespace = "http://example.com/api/v1.0/")]
[ServiceErrorBehaviour(typeof(HttpErrorHandler))]
public class MyServiceService
{
// ...
}
When creating a BehaviorExtensionElement it is even possible to activate the behavior using config:
public class ErrorBehaviorExtensionElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(ServiceErrorBehaviourAttribute); }
}
protected override object CreateBehavior()
{
return new ServiceErrorBehaviourAttribute(typeof(HttpErrorHandler));
}
}
Config:
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="elmah" type="Namespace.ErrorBehaviorExtensionElement, YourAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
<behaviors>
<serviceBehaviors>
<behavior>
<elmah />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
That way it is also possible to use ELMAH in combination with RIA services!
I have done this based on Will's work but I want to verify that this is the correct approach before posting the code.
I think this is a great approach (kudos to Will for this posting!). I don't think Will or you have missed anything here. Implementing IErrorHandler is the preferred way of capturing all possible server-side exceptions that could otherwise cause the communication channel to be faulted (torn down) and thus it's a natural place to hook in some logging like ELMAH.
Marc
This may well be obvious to some people but I just spent quite a while trying to figure out why my HttpContext.Current was null despite following all of Will Hughes' excellent answer. Embarassingly, I realised that this was because my WCF service is activated by a MSMQ message.
I ended up rewriting the ProvideFault()
method:
if (HttpContext.Current == null)
{
ErrorLog.GetDefault(null).Log(new Error(error));
}
else
{
ErrorSignal.FromCurrentContext().Raise(error);
}
I was unable to get the proposed answer working with a WCF Data Service. I wired up the behavior attribute, etc, but still did not get any errors logged. Instead, I ended up adding the following to the service implementation:
protected override void HandleException(HandleExceptionArgs args)
{
Elmah.ErrorSignal.FromCurrentContext().Raise(args.Exception);
base.HandleException(args);
}
I haven't tried doing this explicitly with the REST stuff, and haven't used ELMAH myself, but another option worth looking into might be to hook into WCF using an IDispatchMessageInspector instead of an IErrorHandler.
来源:https://stackoverflow.com/questions/895901/exception-logging-for-wcf-services-using-elmah