Report and swallow is a last-chance exception-handling strategy, i.e. a strategy for exceptions that could not be handled deeper in the call stack. In general, swallowing a last-chance exception is not a good idea because it may leave your application in an invalid state. Also, using an aspect to implement a last-chance exception handler is generally not a good idea because it is simpler to use the AppDomain.CurrentDomain.UnhandledException event.
However, there are some cases where you need to handle last-chance exceptions with a try...catch
block. For instance,
when your code is a plug-in in some host application like Office or Visual Studio, exceptions that are not handled by
Visual Studio Extensions have a chance to crash the whole Visual Studio without any error message.
In this case, you must implement exception handling at each entry point of your API. This includes:
- any interface of the host that your plug-in implements,
- any handlers to host events your plug-in subscribes to,
- any Winforms/WPF entry point.
Since there can be hundreds of such entry points, it is useful to automate this pattern using an aspect.
In this example, we will assume that we have a host application that defines an interface IPartProvider
that plug-ins
can implement. Let's see what the aspect does with the code:
1public class PartProvider : IPartProvider
2{
3 [ReportAndSwallowExceptions]
4 public string GetPart(string name) => throw new Exception("This method has a bug.");
5}
1using System;
2
3public class PartProvider : IPartProvider
4{
5 [ReportAndSwallowExceptions]
6 public string GetPart(string name) { try
7 {
8 throw new Exception("This method has a bug.");}
9 catch (Exception e) when (_exceptionHandler != null && _exceptionHandler.ShouldHandle(e))
10 {
11 _exceptionHandler.Report(e);
12 return default;
13 }
14 }
15private ILastChanceExceptionHandler? _exceptionHandler;
16
17 public PartProvider(ILastChanceExceptionHandler? exceptionHandler = default(global::ILastChanceExceptionHandler?))
18 {
19 this._exceptionHandler = exceptionHandler;
20 }
21}
As we can see, the ReportAndSwallowExceptions
aspect pulls the ILastChanceExceptionHandler
from the dependency
injection container and adds a try...catch
block to the target method.
Infrastructure code
The aspect uses the following interface:
1public interface ILastChanceExceptionHandler
2{
3 /// <summary>
4 /// Determines if an exception should be handled. Typically returns <c>false</c>
5 /// for exception like <see cref="OperationCanceledException"/>.
6 /// </summary>
7 bool ShouldHandle(Exception e);
8
9 /// <summary>
10 /// Reports the exception to the user or to the crash report service.
11 /// </summary>
12 void Report(Exception e);
13}
Aspect code
The ReportAndSwallowExceptionsAttribute
aspect is rather simple:
1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3
4#pragma warning disable CS0649
5
6public class ReportAndSwallowExceptionsAttribute : OverrideMethodAspect
7{
8 [IntroduceDependency(IsRequired = false)]
9 private readonly ILastChanceExceptionHandler? _exceptionHandler;
10
11 public override dynamic? OverrideMethod()
12 {
13 try
14 {
15 return meta.Proceed();
16 }
17 catch (Exception e) when (this._exceptionHandler != null &&
18 this._exceptionHandler.ShouldHandle(e))
19 {
20 this._exceptionHandler.Report(e);
21
22 return default;
23 }
24 }
25}
If you have read the previous examples, the following notes should be redundant.
The ReportAndSwallowExceptionsAttribute
class derives from the OverrideMethodAspect
abstract class, which in turn derives from the System.Attribute class. This
makes ReportAndSwallowExceptionsAttribute
a custom attribute.
The ReportAndSwallowExceptionsAttribute
class implements
the OverrideMethod method. This method acts like a template.
Most of the code in this template is injected into the target method, i.e., the method to which we add
the [ReportAndSwallowExceptionsAttribute]
custom attribute.
Inside the OverrideMethod implementation, the call
to meta.Proceed()
has a very special meaning. When the aspect is applied to the target, the call to meta.Proceed()
is replaced by the original implementation, with a few syntactic changes to capture the return value.
To remind you that meta.Proceed()
has a special meaning, it is colored differently than the rest of the code. If you
use Metalama Tools for Visual Studio, you will also enjoy syntax highlighting in this IDE.
Around the call to meta.Proceed()
, we have a try...catch
exception handler. The catch
block has a when
clause
that should ensure that we do not handle exceptions that are accepted by the host,
typically OperationCanceledException. Then, the catch
handler simply reports the exception and does not
rethrow it.
The [IntroduceDependency] on the top of
the _exceptionHandler
field does the magic of introducing the field and pulling it from the constructor.