Search code examples
asp.net-coreasp.net-core-mvcasp.net-core-webapi.net-7.0

How to use separate exception handling flow in Web API and MVC controller in a single ASP.NET Core MVC application?


I have an ASP.NET Core MVC project in .NET 7 which contains both Web API controllers and MVC controllers. I want the application should handle the errors differently for Web API exceptions and MVC exceptions. Whenever there are exceptions in the MVC controller flow, the ErrorController should process the exceptions which return views (Error or NotFound). Whenever there are exceptions in the Web API controller flow, the exception should be handled by a global exception-hander middleware which returns a JSON response. I've tried to write code to achieve the above-mentioned scenario but am unable and my solution is only using either ErrorController or ExceptionMiddleware for both flows (depending on where I put the ExceptionMiddleware in the pipeline). Could anyone help me to configure the flow? My code is as below:

ErrorController.cs

public class ErrorController : Controller
{
    [Route("Error/{statusCode}")]
    public IActionResult HttpStatusCodeHandler(int statusCode)
    {
        switch (statusCode)
        {
            case 404:
                ViewBag.ErrorMessage = "Sorry, the resource you requested could not be found";
                break;
            default:
                break;
        }
        return View("NotFound");
    }

    [Route("Error")]
    [AllowAnonymous]
    public IActionResult Error()
    {
        var exceptionDetails = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        
        TempData["ExceptionMessage"] = exceptionDetails?.Error.Message;        

        return View("Error");
    }
}

ExceptionMiddleware.cs

public class ExceptionMiddleware : IMiddleware
{
    private readonly APIResponse _apiResponse;

    public ExceptionMiddleware(APIResponse apiResponse)
    {
        _apiResponse = apiResponse;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch(CustomerNotFoundException ex)
        {
            _apiResponse.StatusCode = System.Net.HttpStatusCode.NotFound;
            _apiResponse.ErrorMessages = new List<string> { ex.Message };
            _apiResponse.IsSuccess = false;

            context.Response.StatusCode = (int)_apiResponse.StatusCode;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(JsonConvert.SerializeObject(_apiResponse));
        }
        catch (Exception ex)
        {
            _apiResponse.StatusCode = System.Net.HttpStatusCode.InternalServerError;
            _apiResponse.ErrorMessages = new List<string> { ex.Message };
            _apiResponse.IsSuccess = false;

            context.Response.StatusCode = (int)_apiResponse.StatusCode;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(JsonConvert.SerializeObject(_apiResponse));
        }
    }
}

APIResponse.cs

public class APIResponse
{
    public APIResponse()
    {
        ErrorMessages = new List<string>();
    }
    public HttpStatusCode StatusCode { get; set; }
    public bool IsSuccess { get; set; } = true;
    public List<string> ErrorMessages { get; set; }
    public object? Result { get; set; }
}

CustomersController.cs (Web API controller)

[ApiController]
public class CustomersController : ControllerBase
{
    private readonly APIResponse _apiResponse;

    public CustomersController(APIResponse apiResponse)
    {
        _apiResponse = apiResponse;
    }

    [Route("api/customers/{id}")]       
    public async Task<ActionResult<APIResponse>> Get(int id)
    {            
        throw new CustomerNotFoundException("The customer is not registered in the system.");

        return Ok(_apiResponse);
    }
}

HomeController.cs (MVC Controller)

public class HomeController : Controller
{
    public IActionResult Index()
    {
        throw new Exception("Unable to render Dashboard.");

        return View();
    }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddTransient<APIResponse>();
builder.Services.AddTransient<ExceptionMiddleware>();

var app = builder.Build();

app.UseExceptionHandler("/Error");
app.UseStatusCodePagesWithReExecute("/Error/{0}");

app.UseHsts();

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMiddleware<ExceptionMiddleware>();
app.UseRouting();

app.UseAuthorization();

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

app.Run();

Solution

  • You can use MapWhen method to run middleware according to the request route

    app.MapWhen(context => context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase), appBuilder =>
    {
        appBuilder.UseApiExceptionHandler();
    });
    
    app.MapWhen(context => !context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase), appBuilder =>
    {
        appBuilder.UseMvcExceptionHandler();
    });