I am trying to create a global IExceptionHandler
to catch unhandled exceptions.
This type of exceptions shouldn't happen if the app is well written, so this middleware does only retrieve some data from the request and send an email to an administrator so he (me) could receive a notification if something very bad happens.
The problem is: it seems that IExceptionHandler
is losing all the request context:
HttpContext.Session
throws an InvalidOperationException, even if the app.UseSession()
has been called in Program.csThis way I don't know how to retrive session info to log it on the email.
For example I would want to log the HttpContext.Session.Id or some data present in the ISession dictionary, or try to get a scoped service to retrieve info from.
Edit: some snippets of the test program. Project created with the Razor Pages template of VS 2022.
public class CustomScopedService
{
public CustomScopedService()
{
//If you place a breakpoint here, then click 'Generate Unhandled Error' button, you will see 2 hits of the breakpoint
//1. Created for the TestModel constructor injection
//2. At GetRequiredService in CustomUnhandledExceptionHandler
}
public string Id { get; } = Guid.NewGuid().ToString();
}
public class CustomUnhandledExceptionHandler : IExceptionHandler
{
public CustomUnhandledExceptionHandler(IServiceScopeFactory serviceScopeFactory)
{
this.ServiceScopeFactory = serviceScopeFactory;
}
public IServiceScopeFactory ServiceScopeFactory { get; }
public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
using var scope = this.ServiceScopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<CustomScopedService>();
var id = service.Id; //Id is changed twice
var user = httpContext.Session.GetString("user"); //throws InvalidOperationException
return ValueTask.FromResult(false);
}
}
@page
@model ExceptionHandlerTest.Pages.TestModel
<form method="post" asp-page-handler="UnhandledError">
<div>Id: @Model.Service.Id</div>
<div><input type="text" name="userName" placeholder="Username" value="@HttpContext.Session.GetString("user")" /></div>
<button class="btn btn-danger">Generate Unhandled Error</button>
</form>
public class TestModel : PageModel
{
public TestModel(CustomScopedService service)
{
this.Service = service;
}
public CustomScopedService Service { get; }
public void OnGet()
{
}
public void OnPostUnhandledError(string? userName)
{
this.HttpContext.Session.SetString("user", userName ?? "foo");
throw new Exception("Unhandled exception test");
}
}
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddScoped<CustomScopedService>();
builder.Services.AddSession(); // <---
builder.Services.AddMemoryCache(); // <---
builder.Services.AddExceptionHandler<CustomUnhandledExceptionHandler>(); // <---
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseExceptionHandler(c => { }); // <---
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession(); // <---
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Firstly, you should make sure the session middleware's order is right before the exception handler.
Secondly, you should not use the using var scope = this.ServiceScopeFactory.CreateScope();
this will create a new scope which will cause this issue. You should use var service = httpContext.RequestServices.GetRequiredService<CustomScopedService>();
.
More details, you could refer to below codes:
public class CustomUnhandledExceptionHandler : IExceptionHandler
{
public CustomUnhandledExceptionHandler(IServiceScopeFactory serviceScopeFactory)
{
this.ServiceScopeFactory = serviceScopeFactory;
}
public IServiceScopeFactory ServiceScopeFactory { get; }
public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
var service = httpContext.RequestServices.GetRequiredService<CustomScopedService>();
var id = service.Id; //Id is changed twice
var user = httpContext.Session.GetString("user"); //throws InvalidOperationException
return ValueTask.FromResult(false);
}
}
Program.cs:
...
builder.Services.AddRazorPages();
builder.Services.AddScoped<CustomScopedService>();
builder.Services.AddSession(); // <---
builder.Services.AddMemoryCache(); // <---
builder.Services.AddExceptionHandler<CustomUnhandledExceptionHandler>(); // <---
var app = builder.Build();
app.UseSession(); // <---
app.UseExceptionHandler(c => { }); // <---
...
Result:
Session:
Test page:
Exception handler: