#5 Emailing Exceptions
Friday, November 23, 2018
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.
Parts
- Part 29: Offset Pager Urls
- Part 28: Offset Pager Start
- Part 27: Mock Context Builder
- Part 26: Mock Repository
- Part 25: Mock Async
- Part 24: Picture Tag Helper
- Part 23: Img DPR Tag Helper
- Part 22: Img Responsive Tag Helper
- Part 21: Img Optimized Display
- Part 20: Img Optimization
- Part 19: Img Lazy Loading
- Part 18: Img Responsive
- Part 17: Bottom Nav
- Part 16: Main Nav Cookie
- Part 15: Main Nav Mobile
- Part 14: Main Nav Search
- Part 13: Main Nav Auth
- Part 12: Main Nav Anchors
- Part 11: Main Nav Logo
- Part 10: Search Results
- Part 9: Search Manager
- Part 8: Search Start
- Part 7: Seeding the Database
- Part 6: Domain Database
- Part 5: Emailing Exceptions
- Part 4: Mailkit
- Part 3: View Renderer
- Part 2: Upgrade to 2.1
- Part 1: Quick Start
Video Correction
- WebUi
- Features
- Messaging
- MessageService.cs
- Messaging
- Features
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));
...
}
}
}
We need a model for our email
- WebUi
- Features
- Messaging
- Email
- ExceptionEmail.cshtml.cs
- Email
- Messaging
- Features
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; }
}
}
- WebUi
- Features
- Messaging
- Email
- ExceptionEmail.cshtml
- Email
- Messaging
- Features
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>
Updating our message service
- WebUi
- Features
- Messaging
- IMessageService.cs
- Messaging
- Features
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);
}
}
- WebUi
- Features
- Messaging
- MessageService.cs
- Messaging
- Features
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);
}
}
}
Notify me of any exceptions
- WebUi
- Infrastructure
- ExceptionLoggingMiddleware.cs
- Infrastructure
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");
}
}
}
}
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>();
...
}
}
}