Search code examples
c#asp.net-corerequest-pipeline

Is it possible to intercept errors thrown by middleware after the controller action is executed?


I'm calling UseExceptionHandler in order to handle errors. This works fine, but not for errors thrown by other (subsequently registered) middleware.

The middleware which exceptions I need to handle is TransactionMiddleware. What it does is to save any changes to the database after a successfully completed call to an action. To be clear - it doesn't just commit a transaction, but also runs all the insert:s/update:s etc. This might fail, for example due to database constraints. (There are also other reasons not to complete the transaction, but they are not included here. Just mentioning that to explain that making the database calls earlier and reducing the TransactionMiddleware to simply commiting won't do the trick.)

Is there a way to NOT start the response before this middleware has run its full course?

My Program.cs

var builder = WebApplication.CreateBuilder(args);
new Startup(builder.Configuration).ConfigureServices(builder.Services);

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        // await ExcludedCode()...
    });
});

app.UseSwagger()
    .UseSwaggerUI();

app.UseRouting()
    .UseCors()
    .UseAuthentication()
    .UseAuthorization()
    .UseMiddleware<LanguageMiddleWare>()
    .UseMiddleware<TransactionMiddleWare>()
    .UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });

app.Run();

My (simplified) TransactionMiddleWare-class

    public class TransactionMiddleWare
    {
        private readonly RequestDelegate next;

        public TransactionMiddleWare(RequestDelegate next)
        {
            this.next = next;
        }

        public async Task Invoke(HttpContext context, IDataContext dataContext)
        {
            try
            {
                await next(context);
            }
            catch (PartialExecutionException) 
            {
                this.Commit(context, dataContext);
                throw;
            }
            this.Commit(context, dataContext);
        }

        private void Commit(HttpContext context, IDataContext dataContext)
        {
            if (this.ShouldTransactionCommited(context))
                dataContext.SaveChanges();
            else
                throw new Exception("Exception example for clarity."); 
        }

        private bool ShouldTransactionBeCommited(HttpContext context)
        {
            return true; // Actual code omitted for brevity.
        }
    }

Example of how my controllers return data (no special stuff):

    [ApiController]
    [Route("advertisment")]
    public class AdvertismentController : ControllerBase
    {
        private readonly IAdvertismentService advertismentService;
        private NLog.ILogger log;

        public AdvertismentController(
            IAdvertismentService advertismentService)
        {
            this.log = LogManager.GetCurrentClassLogger();
            this.advertismentService = advertismentService;
        }

        [HttpPost]
        public Result<Guid> Create([FromForm] CreateAdvertismentMultipartFormModel request)
        {
            var id = this.advertismentService.Create(request);
            return new Result<Guid> { Data = id };
        }
    }

Solution

  • Here is what I ended up with. A change in the TransactionMiddleWare class:

            public async Task Invoke(HttpContext context, IDataContext dataContext)
            {
                context.Response.OnStarting(state => {
                    this.Commit(context, dataContext);
                    return Task.CompletedTask;
                }, context);
    
                try
                {
                    await next(context);
                    this.Commit(context, dataContext);
                }
                catch (PartialExecutionException)
                {
                    this.Commit(context, dataContext);
                    throw;
                }
            }
    

    That way it will be run and any exception will occur while it's still possible to modify the output and produce an error message.