Handle API exceptions differently to non API exceptions. Errors when moving code to an external class library

Viewed 474

I am trying to have different exception handling for my web API controllers and my non web API controllers. In other words I want this statement

app.UseExceptionHandler("/Home/Error");

,which is an extension found in Microsoft.AspNetCore.Diagnostics,

to work for all URLs except those that start with /api. I have written some code that does this and it works when it's included in the web project. However if I move this code to another assembly I get an error.

I have written some code to demo my problem using ASP.NET Core 5.

Program.cs


using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace ExceptionHandling
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}


Startup.cs


using ExceptionHandlingUtils;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace ExceptionHandling
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }

        public void Configure(IApplicationBuilder app)
        {

            app.UseaNonApiExceptionHandler("/Home/Error");  // this is my custom middleware, for implementation see below
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Below is the custom middleware and extension method that calls it. ExceptionHandlerExtensions is pretty much copied from here. And the code for ExceptionHandlerMiddleware.cs is here

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace ExceptionHandling.Middleware
{
    public static class ExceptionHandlerExtensions
    {
        public static IApplicationBuilder UseaNonApiExceptionHandler(this IApplicationBuilder app, string errorHandlingPath)
        {
            return app.UseaNonApiExceptionHandler(new ExceptionHandlerOptions
            {
                ExceptionHandlingPath = new PathString(errorHandlingPath)
            });
        }

        public static IApplicationBuilder UseaNonApiExceptionHandler(this IApplicationBuilder app, ExceptionHandlerOptions options)
        {
            return app.UseMiddleware<NonApiExceptionHandler>(Options.Create(options));
        }
    }
}

NonApiExceptionHandler.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Threading.Tasks;

namespace ExceptionHandling
{
        public class NonApiExceptionHandler
    {
        private readonly ExceptionHandlerMiddleware _exceptionHandlerMiddleware;
        private readonly RequestDelegate _next;

        public NonApiExceptionHandler(
            RequestDelegate next,
            ILoggerFactory loggerFactory,
            IOptions<ExceptionHandlerOptions> options,
            DiagnosticListener diagnosticListener)

        {
            _next = next;
            _exceptionHandlerMiddleware = new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener);

        }

        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Path.StartsWithSegments("/api"))
            {
                await _next(context);
                return;
            }

            await _exceptionHandlerMiddleware.Invoke(context);
        }
    }
}

Here is my controller

using Microsoft.AspNetCore.Mvc;
using System;

namespace ExceptionHandling.Controllers
{
    public class HomeController: Controller
    {
        public IActionResult Index()
        {
            throw new Exception("an error");
        }

        public IActionResult Error()
        {
            return Content("an error has occured");
        }
    }
}

Here is my web API controller

using Microsoft.AspNetCore.Mvc;
using System;

namespace ExceptionHandling.Controllers.api
{
    [Route("api/[controller]")]
    [ApiController]
    public class MyApiController: ControllerBase
    {

        public IActionResult Get()
        {
            throw new Exception("an error");
        }
    }
}

The above code works as expected. I.e. if you navigate to / then the exception is thrown and you are redirected to /home/error and the message is displayed. And if you navigate to /api/myapi the exception is thrown but you are not redirected to /home/error.

Because this is functionality I need in many of my websites I have attempted to put this code into another assembly. In other words I changed my startup file to this

using ExceptionHandlingUtils;  //note the namespace change, this is the same code but in a different 
                                //assembly
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace ExceptionHandling
{
    public class Startup
    {

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }

        public void Configure(IApplicationBuilder app)
        {

            app.UseaNonApiExceptionHandler("/Home/Error");          

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

and moved the 2 custom middleware files to a class library targetting .NET 5, with the namespace ExceptionHandlingUtils. The library has the nuget package Microsoft.AspNetCore.Diagnostics imported. When I run the same code but calling my custom middleware from a separate assembly I get the following error

An error occurred while starting the application.
MissingMethodException: Method not found: 'Void Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware..ctor(Microsoft.AspNetCore.Http.RequestDelegate, Microsoft.Extensions.Logging.ILoggerFactory, Microsoft.Extensions.Options.IOptions`1<Microsoft.AspNetCore.Builder.ExceptionHandlerOptions>, System.Diagnostics.DiagnosticSource)'.
ExceptionHandlingUtils.NonApiExceptionHandler..ctor(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ExceptionHandlerOptions> options, DiagnosticListener diagnosticListener)

MissingMethodException: Method not found: 'Void Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware..ctor(Microsoft.AspNetCore.Http.RequestDelegate, Microsoft.Extensions.Logging.ILoggerFactory, Microsoft.Extensions.Options.IOptions`1<Microsoft.AspNetCore.Builder.ExceptionHandlerOptions>, System.Diagnostics.DiagnosticSource)'.
ExceptionHandlingUtils.NonApiExceptionHandler..ctor(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ExceptionHandlerOptions> options, DiagnosticListener diagnosticListener)
System.RuntimeMethodHandle.InvokeMethod(object target, object[] arguments, Signature sig, bool constructor, bool wrapExceptions)
System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture)
Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider)
Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, object[] parameters)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass5_0.<UseMiddleware>b__0(RequestDelegate next)
Microsoft.AspNetCore.Builder.ApplicationBuilder.Build()
Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
ExceptionHandling.Program.Main(string[] args) in Program.cs
+
            CreateHostBuilder(args).Build().Run();

I would like to know how I can fix this error. Alternatively is there another way to get the ExceptionHandlerMiddleware from Microsoft.AspNetCore.Diagnostics to ignore my web API urls.

1 Answers

Instead of creating your custom middleware class, NonApiExceptionHandler, which creates an instance of ExceptionHandlerMiddleware, I recommend making use of UseWhen, which:

Conditionally creates a branch in the request pipeline that is rejoined to the main pipeline.

Here's a minimal example that uses UseWhen:

app.UseWhen(
   context => !context.Request.Path.StartsWithSegments("/api"),
   nonApiApp => nonApiApp.UseExceptionHandler("/Home/Error"));

The first argument passed into UseWhen represents the condition under which the pipeline will be branched. In this case, UseExceptionHandler will affect requests that don't start with /api.

You can wrap this up in an extension method, as you've done in your question, so that it's reusable. Here's a simple example of that:

public static class ExceptionHandlerExtensions
{
    public static IApplicationBuilder UseaNonApiExceptionHandler(
        this IApplicationBuilder app, string errorHandlingPath)
    {
        return app.UseWhen(
            context => !context.Request.Path.StartsWithSegments("/api"),
            nonApiApp => nonApiApp.UseExceptionHandler(errorHandlingPath));
    }

    // ...
}

For more details about UseWhen, including how it compares to MapWhen, see Branch the middleware pipeline.

Related