Alerts – Part One

Alerts – Part One

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