Background: ASP.NET 5 (ASP.NET Core 1.0) MVC 6 application using Dapper and the Repository Pattern
Obviously, like with every other website/app, I'm trying to eliminate most/all of the exceptions that popup in my website.
I implemented an ExceptionFilter
in order to catch all unhandled exceptions like this:
public class UnhandledExceptionFilter : ActionFilterAttribute, IExceptionFilter
{
private readonly IErrorRepo _errorRepo;
public UnhandledExceptionFilter(IErrorRepo errorRepo)
{
_errorRepo = errorRepo;
}
public void OnException(ExceptionContext context)
{
try
{
_errorRepo.SaveException(context.Exception);
}
catch { }
}
}
This works great when the error comes from C#
code. But I've purposely put in errors in my razor views (cshtml files) and those are NOT getting caught by this filter.
Is there an additional attribute/interface that I need to inherit in order to catch razor exceptions?
UPDATE:
Here's where the filter attribute is specified, in the startup.cs file in the ConfigureServices method.
services.AddMvc(options =>
{
options.Filters.Add(new UnhandledExceptionFilter(new ErrorRepo(Configuration)));
});
The trick to doing this is not in the attribute - it's by adding a middleware provider. Once your middleware is in the pipeline, you'll be able to catch exceptions thrown at any point (so your attribute will no longer be needed).
The Logger
This is the thing that's actually going to log errors. I've copied what I've seen from your IErrorRepo
interface, but of course you could modify it to include any of the additional information passed into the Log
method below.
public class UnhandledExceptionLogger : ILogger
{
private readonly IErrorRepo _repo;
public UnhandledExceptionLogger(IErrorRepo repo)
{
_repo = repo;
}
public IDisposable BeginScopeImpl(object state) =>
new NoOpDisposable();
public bool IsEnabled(LogLevel logLevel) =>
logLevel == LogLevel.Critical || logLevel == LogLevel.Error;
public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func<object, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
_repo.SaveException(exception);
}
}
private sealed class NoOpDisposable : IDisposable
{
public void Dispose()
{
}
}
}
The Provider
This is the factory that will create the logger. It's only going to be instantiated once, and will create all the loggers when it goes through the pipeline.
public class UnhandledExceptionLoggerProvider : ILoggerProvider
{
private readonly IErrorRepo _repo;
public UnhandledExceptionLoggerProvider(IErrorRepo repo)
{
_repo = repo;
}
public ILogger CreateLogger(string categoryName) =>
new UnhandledExceptionLogger(_repo);
public void Dispose()
{
}
}
Registering the Provider
Once added to the ILoggerFactory
, it will be invoked on each request in the pipeline. Often this is done through a custom extension method, but we've already got a lot of new code so I'll omit that part.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddProvider(new UnhandledExceptionLoggerProvider(new ErrorRepo()));
// the rest
I handle all exception (including Razor ones) with the following code:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
var exceptionHandlingOptions = new ExceptionHandlerOptions()
{
ExceptionHandler = ExceptionHandler.OnException // !! this is the key line !!
// ExceptionHandlingPath = "/Home/Error"
// according to my tests, the line above is useless when ExceptionHandler is set
// after OnException completes the user would receive empty page if you don't write to Resonse in handling method
// alternatively, you may leave ExceptionHandlingPath without assigning ExceptionHandler and call ExceptionHandler.OnException in controller's action instead
// write me if you need a sample code
};
app.UseExceptionHandler(exceptionHandlingOptions);
}
public static class ExceptionHandler
{
public static async Task OnException(HttpContext context)
{
var feature = context.Features.Get<IExceptionHandlerFeature>();
var exception = feature?.Error;
if (exception == null) return;
//TODO: log exception here
}
}
Do not forget to remove IExceptionFilter as it would handle some of the exceptions as well.