Advertisement

#5 Emailing Exceptions

In this article we will create both a template and model that we can use to generate an email from an exception. With that in place we will then create a middleware that we can use to send an email automatically if our application encounters an unhandled exception. And of course to do this the last step will be to add our new middleware to the pipeline within our Startup class.

Video Correction

  • WebUi
    • Features
      • Messaging
        • MessageService.cs

In the video we create a method in the message service class for sending exception emails. In that method I specified which template for the view renderer to use incorrectly. I thought that I had specified the email folder as a view location within the view location expanderer but that is not correct. The two ways to correct the mistake is to either add that location to the expander or to modify the path within the send exception method which is the choice I am making here (a).

MessageService.cs

...
namespace WebUi.Features.Messaging
{
    public class MessageService : IMessageService
    {
        ...
        public async Task SendExceptionEmailAsync(Exception e, HttpContext context)
        {
            var message = _viewRenderer.Render("Features/Messaging/Email/ExceptionEmail", new ExceptionEmailModel(e, context));
            ...
        }
    }
}
(a) Correcting the error of specifying the location of the exception email location.

We need a model for our email

  • WebUi
    • Features
      • Messaging
        • Email
          • ExceptionEmail.cshtml.cs

In order to populate our email with the information that we want we need to create a model to hold the information (b). The ultimate decision of what information you want to include and in what format it should be is completely up to you.

ExceptionEmail.cshtml.cs

using System;
using System.Collections;
using System.Diagnostics;
using Microsoft.AspNetCore.Http;

namespace WebUi.Features.Messaging.Email
{
    public class ExceptionEmailModel
    {
        public ExceptionEmailModel(Exception e, HttpContext context)
        {
            var trace      = new StackTrace(e, true);
            Frames         = trace.GetFrames();
            ExceptionData  = e.Data;
            Headers        = context.Request.Headers;
            HelpLink       = e.HelpLink;
            HResult        = e.HResult;
            InnerException = e.InnerException?.Message ?? string.Empty;
            Message        = e.Message;
            Source         = e.Source;
            StackTrace     = e.StackTrace;
            TargetSite     = e.TargetSite.ToString();
            Time           = $"{DateTime.Now.ToLongDateString()} at {DateTime.Now.ToLongTimeString()}";
            Type           = e.GetBaseException().GetType().FullName;
            Url            = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
        }

        public IDictionary ExceptionData { get; set; }
        public StackFrame[] Frames { get; set; }
        public IHeaderDictionary Headers { get; set; }
        public string HelpLink { get; set; }
        public int HResult { get; set; }
        public string InnerException { get; set; }
        public string Message { get; set; }
        public string Source { get; set; }
        public string StackTrace { get; set; }
        public string TargetSite { get; set; }
        public string Time { get; set; }
        public string Type { get; set; }
        public string Url { get; set; }
        public string User { get; set; }
    }
}
(b) The model that we will use to hold the information that we want to include in our email.
  • WebUi
    • Features
      • Messaging
        • Email
          • ExceptionEmail.cshtml

Now that we have a model we are free to create the actual email text itself (c). Once again the format and information contained within the email is up to you but the code we have here should work as a starting point.

ExceptionEmail.cshtml

@using System.Collections
@model WebUi.Features.Messaging.Email.ExceptionEmailModel

<!DOCTYPE HTML>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Exception</title>
    <style>
        table {
            border-collapse: collapse;
            margin: 0.75em 0;
            table-layout: fixed;
            width: 100%;
            font-size: 16px;
        }
        tbody {
            margin: 0;
            padding: 0;
            border: 0;
            font-size: 100%;
            font: inherit;
            vertical-align: baseline;
        }
        tr, td, th {
            vertical-align: middle;
        }
        td {
            border-bottom: 1px solid black;
            padding: 0.75em 0;
        }
    </style>
</head>
<body>

<table>
    <tbody>
    <tr>
        <td>Time</td>
        <td>@Model.Time</td>
    </tr>
    <tr>
        <td>Message</td>
        <td>@Model.Message</td>
    </tr>
    <tr>
        <td>Frames</td>
        <td>
            @foreach (var frame in Model.Frames)
            {
                var method = frame.GetMethod();
                
                var fullName = method.DeclaringType != null
                    ? $"{method.DeclaringType.FullName}.{method.Name}"
                    : method.Name;
                <div style="font-weight: bold;">@fullName</div>
                <div style="padding-left: 20px; font-size: small;">@frame</div>
            }
        </td>
    </tr>
    <tr>
        <td>StackTrace</td>
        <td>@Model.StackTrace</td>
    </tr>
    <tr>
        <td>Type</td>
        <td>@Model.Type</td>
    </tr>
    <tr>
        <td>HelpLink</td>
        <td>@Model.HelpLink</td>
    </tr>
    <tr>
        <td>HResult</td>
        <td>@Model.HResult</td>
    </tr>
    <tr>
        <td>InnerException</td>
        <td>@Model.InnerException</td>
    </tr>
    <tr>
        <td>Source</td>
        <td>@Model.Source</td>
    </tr>
    <tr>
        <td>TargetSite</td>
        <td>@Model.TargetSite</td>
    </tr>
    <tr>
        <td>Url</td>
        <td>@Model.Url</td>
    </tr>
    <tr>
        <td>User</td>
        <td>@User.Identity.Name</td>
    </tr>
    @foreach (var header in Model.Headers)
    {
        <tr>
            <td>@header.Key</td>
            <td>@header.Value</td>
        </tr>
    }
    @foreach (DictionaryEntry data in Model.ExceptionData)
    {
        <tr>
            <td>@data.Key</td>
            <td>@data.Value</td>
        </tr>
    }
    </tbody>
</table>
</body>
</html>
(c) The code that will be used to generate the actual body of our exception email.
Advertisement

Updating our message service

  • WebUi
    • Features
      • Messaging
        • IMessageService.cs

We could of course just use the method that we previously defined on our message service to send the exception email but I would like to keep all code that generates an email in the same place so we will add a couple of new methods to our interface. One of the methods will deal directly with exceptions and the other will deal with sending of emails to our support address (d).

IMessageService.cs

using System;
...
using Microsoft.AspNetCore.Http;

namespace WebUi.Features.Messaging
{
    public interface IMessageService
    {
        ...
        Task SendEmailToSupportAsync(string subject, string message);
        Task SendExceptionEmailAsync(Exception e, HttpContext context);
    }
}
(d) Updating our message service interface to add two new method signatures.
  • WebUi
    • Features
      • Messaging
        • MessageService.cs

With the interface updated if we ever want our program to run again we need to update our message service itself (a). For the method sending an email to our support address it is there just to add sensible information for the to and from name/address and just pass the subject and message through. The exception email method will make use of the view renderer that we created a few articles back to take an instance of our ExceptionEmailModel and plug it into our ExceptionEmail template so that we can send it along as the message to our email to support method.

MessageService.cs

using System;
...
using Microsoft.AspNetCore.Http;
...
using WebUi.Features.Messaging.Email;
using WebUi.Infrastructure;

namespace WebUi.Features.Messaging
{
    public class MessageService : IMessageService
    {
        private readonly IViewRenderer _viewRenderer;

        public MessageService(IViewRenderer viewRenderer)
        {
            _viewRenderer = viewRenderer;
        }
        ...
        public async Task SendEmailToSupportAsync(string subject, string message)
        {
            await SendEmailAsync("No Reply", "no-reply@yourdomain.com", "Support", "support@yourdomain.com", subject, message);
        }

        public Task SendExceptionEmailAsync(Exception e, HttpContext context)
        {
            var message = _viewRenderer.Render("Features/Messaging/Email/ExceptionEmail", new ExceptionEmailModel(e, context));
            await SendEmailToSupport("Exception", message);
        }
    }
}
(a) Our message service now makes it easier to send emails to our support address as well as handling sending emails for any exceptions that we pass it.

Notify me of any exceptions

  • WebUi
    • Infrastructure
      • ExceptionLoggingMiddleware.cs

I am a bit paranoid about things going wrong and me not knowing about it. To help prevent this from happening I would like to be notified whenever an exception occurs. To do this we are going to create an exception logging middleware (e). As the name implies anytime an exception occurs the middleware will take care of logging it. In this case logging it means sending an email.

ExceptionLoggingMiddleware.cs

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using WebUi.Features.Messaging;

namespace WebUi.Infrastructure
{
    public class ExceptionLoggingMiddleware
    {
        private readonly IHostingEnvironment _env;
        private readonly IMessageService _messageService;
        private readonly RequestDelegate _next;

        public ExceptionLoggingMiddleware(RequestDelegate next, IHostingEnvironment env, IMessageService messageService)
        {
            _env = env;
            _messageService = messageService;
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception e)
            {
                if (_env.IsDevelopment())
                {
                    throw;
                }

                await _messageService.SendExceptionEmailAsync(e, context);
                // Redirect the user to whatever the appropriate url for an unhandled exception is for your application
                context.Response.Redirect("https://www.yourdomain.com/500");
            }
        }
    }
}
(e) The exception logging middleware will take care of automatically sending an email whenever an exception is thrown.

Add the middleware to the pipeline

  • WebUi
    • Startup.cs

If we want an email to be sent to us automatically we need to add the middleware to the pipeline. To do this we just need to add a single line to the Configure method in our Startup class (f).

Startup.cs

...
namespace WebUi
{
    public class Startup
    {
        ...
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                ...
            }
            else
            {
                //app.UseExceptionHandler("/Error"); <-- Commented out
                ...
            }

            ...
            app.UseMiddleware<ExceptionLoggingMiddleware>();
            ...
        }
    }
}
(f) Adding the exception logging middleware to the pipeline.
Exciton Interactive LLC
Advertisement