Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 244
Dave Barnett Avatar asked Feb 17 '26 00:02

Dave Barnett


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.

like image 102
Kirk Larkin Avatar answered Feb 18 '26 12:02

Kirk Larkin