最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - IStartupFilter for exception handling in .NET integration tests not capturing the exceptions - Stack Overflow

programmeradmin10浏览0评论

I have an integration test using a WebapplicationFactory in .Net 9 to spin up the main Program.cs and hit endpoints. My problem is often an endpoint will return a generic 500 error when an unhandled exception is thrown - I want to log the stacktrace when this happens.

I've been trying to use IStartupFilter to add an exception handling middleware, but I can't seem to get it working. I imagine my code is capturing exceptions earlier in the pipeline somehow - is there a way to get my code to run first? or to override the exception handler somehow?

This is the code I have tried to use:

public class ExceptionHandlingStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            builder.UseMiddleware<ExceptionHandlingMiddleware>();
            next(builder);
        };
    }
}

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionHandlingMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception ex)
        {
            var endpoint = context.GetEndpoint()?.DisplayName ?? "Unknown endpoint";
            Log.Error(ex, $"Unhandled exception at {endpoint}");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("An error occurred.");
        }
    }
}

And then in my WebApplicationFactory

    builder.ConfigureTestServices(services =>{
        services.AddTransient<IStartupFilter, ExceptionHandlingStartupFilter>();
    }

Is there a better way to do this?

For reference, I've confirmed that my await _next.Invoke does get triggered, but when I press next in the debugger it never re-enters this scope to go to the next line, and definitely never makes it to the catch block.

Update, I've now tried using an IExceptionFilter to achieve this result. It is also not being triggered - possibly the main code is still absorbing the error at a lower level.

New code:

public class ExceptionHandlingStartupFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context?.Exception != null)
        {
            var endpoint = context.HttpContext?.Request?.Path;
            Log.Error(
                context.Exception,
                "An exception occurred while processing the request for {Endpoint}",
                endpoint
            );
        }
    }
}

and in my WebApplicationFactory

services.Configure<MvcOptions>(options =>
{
    options.Filters.Add(new ExceptionHandlingStartupFilter());
});

Adding a debugger to the if statement shows it isn't getting hit either.

Final update - now that I know what to look for:

I've discovered why I was struggling so much, the setup code on the platform I'm working with sets up two exception handlers - one that uses an IActionFilter, and one that uses an IExceptionFilter.

So the code execution was:

  1. the framework exception filter
  2. my exception filter
  3. the framework action filter
  4. the endpoint
  5. the framework action filter (captures the ex)

Hence none of my exception filters were working, hence my answer below being the fix.

I have an integration test using a WebapplicationFactory in .Net 9 to spin up the main Program.cs and hit endpoints. My problem is often an endpoint will return a generic 500 error when an unhandled exception is thrown - I want to log the stacktrace when this happens.

I've been trying to use IStartupFilter to add an exception handling middleware, but I can't seem to get it working. I imagine my code is capturing exceptions earlier in the pipeline somehow - is there a way to get my code to run first? or to override the exception handler somehow?

This is the code I have tried to use:

public class ExceptionHandlingStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            builder.UseMiddleware<ExceptionHandlingMiddleware>();
            next(builder);
        };
    }
}

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionHandlingMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception ex)
        {
            var endpoint = context.GetEndpoint()?.DisplayName ?? "Unknown endpoint";
            Log.Error(ex, $"Unhandled exception at {endpoint}");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("An error occurred.");
        }
    }
}

And then in my WebApplicationFactory

    builder.ConfigureTestServices(services =>{
        services.AddTransient<IStartupFilter, ExceptionHandlingStartupFilter>();
    }

Is there a better way to do this?

For reference, I've confirmed that my await _next.Invoke does get triggered, but when I press next in the debugger it never re-enters this scope to go to the next line, and definitely never makes it to the catch block.

Update, I've now tried using an IExceptionFilter to achieve this result. It is also not being triggered - possibly the main code is still absorbing the error at a lower level.

New code:

public class ExceptionHandlingStartupFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context?.Exception != null)
        {
            var endpoint = context.HttpContext?.Request?.Path;
            Log.Error(
                context.Exception,
                "An exception occurred while processing the request for {Endpoint}",
                endpoint
            );
        }
    }
}

and in my WebApplicationFactory

services.Configure<MvcOptions>(options =>
{
    options.Filters.Add(new ExceptionHandlingStartupFilter());
});

Adding a debugger to the if statement shows it isn't getting hit either.

Final update - now that I know what to look for:

I've discovered why I was struggling so much, the setup code on the platform I'm working with sets up two exception handlers - one that uses an IActionFilter, and one that uses an IExceptionFilter.

So the code execution was:

  1. the framework exception filter
  2. my exception filter
  3. the framework action filter
  4. the endpoint
  5. the framework action filter (captures the ex)

Hence none of my exception filters were working, hence my answer below being the fix.

Share Improve this question edited Mar 30 at 21:43 The Lemon asked Mar 27 at 3:48 The LemonThe Lemon 1,41916 silver badges29 bronze badges 3
  • 1 Is this really for .NET / ASP.NET - the legacy, Windows-only versions before .NET 4.8 ? The code looks and "smells" like .NET Core / ASP.NET Core - if so - please use the correct tags to let us know! – marc_s Commented Mar 27 at 5:08
  • 1 If this is ASP.NET Core rather than ASP.NET, why would you choose IStartupFilter over IExceptionFilter? – ProgrammingLlama Commented Mar 27 at 5:10
  • @ProgrammingLlama Mainly because I hadn't discovered it - but that's not working for me either I'm afraid. Let me update my question with the new code – The Lemon Commented Mar 27 at 5:19
Add a comment  | 

2 Answers 2

Reset to default 2

Personally, I'd use an IExceptionFilter:

public class ExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        var endpoint = context.HttpContext.GetEndpoint()?.DisplayName ?? "Unknown endpoint";
        Console.WriteLine($"Error: {endpoint}");
        context.Result = new ObjectResult("An error occurred.")
        {
            StatusCode = (int)HttpStatusCode.InternalServerError
        };
    }
}

Registration:

builder.Services.Configure<MvcOptions>(c =>
{
    c.Filters.Add<ExceptionFilter>();
});

I've tested and confirmed that this works with ASP.NET Core 8.0, but it should also work with 9.0.

So the source of the problem was something in the main application code was capturing the exception before it could reach my filters and middleware. Even adding an IExceptionFilter still ran after whatever filter was being added in the production code. Seeing as I can't alter the production code middleware for my testing code (at least I want to avoid it if at all possible), the solution was to just inject code that runs before whatever filter is suppressing the errors.

https://learn.microsoft/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-9.0

Looking at the execution order, IActionFilter runs before any exception handlers can run and hijack the pipeline. It wouldn't be good to do in production code, but for my testing framework the following code worked perfectly:

In my builder.ConfigureTestServices, I have:

private void AddTestMiddleware(IServiceCollection services)
{
    services.Configure<MvcOptions>(options =>
    {
        options.Filters.Add(new ExceptionHandlingStartupFilter());
    });
}

And this is my ExceptionHandlingStartupFilter:

public class ExceptionHandlingStartupFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context) { }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Exception != null)
        {
            var endpoint = context.HttpContext?.Request?.Path;
            Log.Error(
                context.Exception,
                "An exception occurred while processing the request for {Endpoint}",
                endpoint
            );
        }
    }
}
发布评论

评论列表(0)

  1. 暂无评论