If you’re anything like me, you tend to write your fair share of error handlers. Most of the code I write is on the back-end. That means, for me at least, I tend to write a lot of handlers to do things like log errors, write errors to a console window, email errors, perhaps even text errors that are really serious. Not only that, but I also write copious amounts of code to handle other kinds of events, as well. Things like trace handlers, debug handlers, warnings, information, etc., etc.
What I need, and I suspect, most developers need, is a way to collapse at least some of that duplicated code into a reusable abstraction. But, since error handling is so dependent on the project it used in, that means the overall scheme must be flexible, extensible, loosely coupled, yadda, yadda…
After trying a few approaches, in several of my larger projects, I finally decided on the scheme I’ll write about today. I won’t claim that this is always best technique for handling errors, but, generally speaking, it seems to work well enough for me.
After thinking about things for a while, I decided to start with a placeholder interface – literally an empty type that I could then go back and hang functionality off of, as needed, using extension methods. What’s more, I decided that whatever class I created to implement that interface should probably be a singleton. This is the pattern I chose for my validations service (The IGuard
type, from CG.Validations
) and it’s worked extremely well over the years.
Here is the interface I started with:
public interface IAlert { IAlertHandler Handler { get; } }
So, I’m saying there is a type called IAlert
, that contains an IAlertHandler
object the property called Handler
. That handler is essentially a delegate that I’ll eventually call to raise alerts. Let’s go look at the IAlertHandler
type:
public interface IAlertHandler { void HandleAlert( AlertType alertType, IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); }
IAlertHandler
contains a single method, HandleAlert
, that is typically called to raise an alert event at runtime. Notice that the first parameter is of type AlertType
. That enumeration lists all the different alert types I’ll support. Here is what that type looks like:
public enum AlertType { Information = 0, Warning = 1, Error = 2, Critical = 3, Audit = 4, Debug = 5, Trace = 6 }
So there are seven types of alerts that I’ll recognize. Now, what happens when any of these types is raised is completely dependent on the handler delegate, but, generally speaking, I’ve categorized alerts into these seven types. If I need to add additional types later, I can, but I think that list should be reasonably enough, for now.
Having presented the interface types, let’s move on to look at the classes that correspond with those interfaces. Let’s start with the Alert
class, which looks like this:
public sealed class Alert : SingletonBase<Alert>, IAlert { public IAlertHandler Handler { get; internal set; } private Alert() { this.SetHandler( new DefaultAlertHandler() ); } }
I’ll point out that this class derives from my SingletonBase
class, which is part of the CODEGATOR CG.Core
NUGET Package. A detailed description of SingletonBase
is beyond the scope of this blog entry, but, just know that it means that the Alert
class will be a singleton type, with a public Instance
property that returns the IAlert
instance.
Otherwise, the Alert
class simply implements the Handler
property we first saw on the IAlert
type. There is also a private constructor, where the singular Alert
instance initializes itself with a default IAlertHandler
object. The SetHandler
method itself is an extension method hung off the IAlert
type. Let’s go look at that now:
public static void SetHandler<T>( this Alert alert, T handler ) where T : IAlertHandler { Guard.Instance().ThrowIfNull(alert, nameof(alert)) .ThrowIfNull(handler, nameof(handler)); alert.Handler = handler; }
So all SetHandler
does, really, is validate any handler object passed into it. After that it sets the Handler
property on the IAlert
instance, with the handler value.
Let’s go look at the DefaultAlertHandler
type next. It looks something like this:
public class DefaultAlertHandler : AlertHandlerBase, IAlertHandler { protected override void HandleInformation( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(args, nameof(args)); var previousColor = Console.ForegroundColor; try { Console.ForegroundColor = ConsoleColor.White; Console.WriteLine( $"{DateTime.Now} [Information] -> {args["message"]}" ); } finally { Console.ForegroundColor = previousColor; } } protected override void HandleWarning( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(args, nameof(args)); var previousColor = Console.ForegroundColor; try { Console.ForegroundColor = ConsoleColor.DarkYellow; if (args.ContainsKey("ex")) Console.WriteLine( $"{DateTime.Now} [Warning] -> {args["message"]}, {(args["ex"] as Exception).Message}" ); else Console.WriteLine( $"{DateTime.Now} [Warning] -> {args["message"]}" ); } finally { Console.ForegroundColor = previousColor; } } protected override void HandleError( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(args, nameof(args)); var previousColor = Console.ForegroundColor; try { Console.ForegroundColor = ConsoleColor.DarkRed; if (args.ContainsKey("ex")) Console.WriteLine( $"{DateTime.Now} [Error] -> {args["message"]}, {(args["ex"] as Exception).Message}" ); else Console.WriteLine( $"{DateTime.Now} [Error] -> {args["message"]}" ); } finally { Console.ForegroundColor = previousColor; } } protected override void HandleCritical( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(args, nameof(args)); var previousColor = Console.ForegroundColor; try { Console.ForegroundColor = ConsoleColor.Red; if (args.ContainsKey("ex")) Console.WriteLine( $"{DateTime.Now} [Critical] -> {args["message"]}, {(args["ex"] as Exception).Message}" ); else Console.WriteLine( $"{DateTime.Now} [Critical] -> {args["message"]}" ); } finally { Console.ForegroundColor = previousColor; } } protected override void HandleDebug( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(args, nameof(args)); Debug.WriteLine( $"{DateTime.Now} [Debug] -> {args["message"]}" ); } protected override void HandleAudit( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(args, nameof(args)); var previousColor = Console.ForegroundColor; try { Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine( $"{DateTime.Now} [Audit] -> {args["message"]}" ); } finally { Console.ForegroundColor = previousColor; } } protected override void HandleTrace( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(args, nameof(args)); var previousColor = Console.ForegroundColor; try { Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine( $"{DateTime.Now} [Trace] -> {args["message"]}" ); } finally { Console.ForegroundColor = previousColor; } } }
As we can see, the DefaultAlertHandler
class implements the IAlertHandler
interface, and derives from the AlertHandlerBase
class. I’ll look at the AlertHandlerBase
class shortly, but, for now, let’s stay focused on the DefaultAlertHandler
class.
The first method on the DefaultAlertHandler
class is called HandleInformation
. That method accepts a dictionary of named object arguments, as well as the name of the calling method, and the source file path, and the line number where the HandleInformation
method was called from. The first thing the method does is validate the table of named arguments. After that, it gets the existing color of the console’s foreground, then it changes that color, writes the message
argument to the console, and finally, resets the console’s foreground color again.
That’s all well and good, but, just looking at this method, it doesn’t seem like a terribly useful way to handle an information alert. Keep in mind though, the name of this class is DefaultAlertHandler
. That means these handlers are what will be called, whenever an alert is raised, when no other handler has been set for the project. That’s typically at application startup, before the app has a chance to create the host, read configuration settings, create logs, email services, SMTP services, etc., etc. So, from that perspective, this is probably a reasonable way to handle informational alerts that are raised very, very early in the application’s life cycle.
The other methods on DefaultAlertHandler
are all some variation of what I’ve done for HandleInformation
. Because of that, I won’t waste everyone’s time by walking through each one.
Let’s go look at the AlertHandlerBase
class now, which is the class I’ll use as a base for any other alert handlers I create, down the road. Here is that class:
public abstract class AlertHandlerBase : IAlertHandler { // ******************************************************************* // Public methods. // ******************************************************************* #region Public methods public virtual void HandleAlert( AlertType alertType, IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ) { switch (alertType) { case AlertType.Audit: HandleAudit(args, memberName, sourceFilePath, sourceLineNumber); break; case AlertType.Critical: HandleCritical(args, memberName, sourceFilePath, sourceLineNumber); break; case AlertType.Debug: HandleDebug(args, memberName, sourceFilePath, sourceLineNumber); break; case AlertType.Error: HandleError(args, memberName, sourceFilePath, sourceLineNumber); break; case AlertType.Information: HandleInformation(args, memberName, sourceFilePath, sourceLineNumber); break; case AlertType.Trace: HandleTrace(args, memberName, sourceFilePath, sourceLineNumber); break; case AlertType.Warning: HandleWarning(args, memberName, sourceFilePath, sourceLineNumber); break; } } protected abstract void HandleInformation( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); protected abstract void HandleWarning( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); protected abstract void HandleError( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); protected abstract void HandleCritical( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); protected abstract void HandleDebug( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); protected abstract void HandleAudit( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); protected abstract void HandleTrace( IDictionary<string, object> args, [CallerMemberName] string memberName = null, [CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0 ); }
The AlertHandlerBase
class implements the HandleAlert
method. It uses the alertType
parameter in a switch statement, to decide how best to route the event. No matter what type of alert it encounters, there is a corresponding abstract method to be called. For instance, Error
alerts are processed by calling the HandleError
method, whereas Trace
alerts are handled by calling the HandleTrace
method. Since all the Handle
* methods are abstract, it’s up to derived classes to add some meaningful implementation.
The only other code to review are some of the extension methods I’ve added, just to make working with the IAlert
type a little easier. The first is the TryRaise
method, which attempts to raise an alert, and returns false if that attempt fails:
public static bool TryRaise( this IAlert alert, AlertType alertType, IDictionary<string, object> args = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(alert, nameof(alert)) .ThrowIfNull(args, nameof(args)); try { alert.Handler.HandleAlert( alertType, args, memberName, sourceFilePath, sourceLineNumber ); } catch { return false; } return true; }
Not much to say about TryRaise
, other than it calls the Handler
property, on the IAlert
instance, to get the IAlertHandler
object. From there, it calls the HandleAlert
method to send the alert event on it’s way.
There is another extension method, this time called Raise
, that does essentially the same thing TryRaise
does, except is has no return value and doesn’t catch any exceptions that might be generated by the alert handler itself. Here is that method:
public static void Raise( this IAlert alert, AlertType alertType, IDictionary<string, object> args = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0 ) { Guard.Instance().ThrowIfNull(alert, nameof(alert)) .ThrowIfNull(args, nameof(args)); alert.Handler.HandleAlert( alertType, args, memberName, sourceFilePath, sourceLineNumber ); }
What I’ve shown, so far, is enough to have a completely functional alert handling abstraction. Now, granted, you’d probably want to immediately create a more robust handler class, but, that takes nothing away from the fact that I can raise alerts, and process them, in an highly extensible, loosely coupled fashion, using this code.
Still, it might be nice to provide some more extensions methods – just so I don’t force everyone to deal with the AlertType
parameter on the Raise
and/or TryRaise
methods. For that reason, I also created several other extension methods that all look something like this:
public static IAlert RaiseInformation( this IAlert alert, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0 ) { alert.TryRaise( AlertType.Information, new Dictionary<string, object>() { { nameof(message), message } }, memberName, sourceFilePath, sourceLineNumber ); return alert; }
This particular method, RaiseInformation
, obviously raises an informational alert and passes in a string message. But, there are also variations for the other alert types, too. Methods like RaiseAudit RaiseWarning
, RaiseError
, etc., etc. The biggest difference between most of these methods are the type of alert they raise.
A few variations of Raise*
also contain additional parameters, where they make sense. For example, here is a RaiseError
variation that accepts a string
parameter as well as an Exception
parameter:
public static IAlert RaiseError( this IAlert alert, string message, Exception ex, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0 ) { alert.TryRaise( AlertType.Error, new Dictionary<string, object>() { { nameof(message), message }, { nameof(ex), ex } }, memberName, sourceFilePath, sourceLineNumber ); return alert; }
This flexibility, for adding additional parameters to the method(s), is one of the reasons I like the technique of starting with an empty interface and then adding behavior later, using extension methods.
So that’s about it really. Not much code for this abstraction, but, a great deal of flexibility.
How should it be called? Good question! Here’s a quick example of what I usually do, in my projects:
void MyFunction() { try { // some code that might throw exceptions } catch (Exception ex) { Alert.Instance().RaiseError(ex, } }
I’ll follow this blog post up with another one where I’ll describe my standard alert handler class. Watch for that to land soon.
As always, my source code is available, on GITHUB, HERE. The NUGET package itself can be downloaded, from NUGET, HERE.
Photo by John Cafazza on Unsplash