Ultimate ASP.NET Core Web API 5 GLOBAL ERROR HANDLING

5 GLOBAL ERROR HANDLING

5 全局错误处理

Exception handling helps us deal with the unexpected behavior of our system. To handle exceptions, we use the try-catch block in our code as well as the finally keyword to clean up our resources afterward.‌
异常处理有助于我们处理系统的意外行为。为了处理异常,我们在代码中使用 try-catch 块,然后使用 finally 关键字来清理我们的资源。

Even though there is nothing wrong with the try-catch blocks in our Actions and methods in the Web API project, we can extract all the exception handling logic into a single centralized place. By doing that, we make our actions cleaner, more readable, and the error handling process more maintainable.
即使 Web API 项目中的 Actions 和 methods 中的 try-catch 块没有问题,我们也可以将所有异常处理逻辑提取到一个集中的位置。通过这样做,我们使我们的作更清晰、更具可读性,并且错误处理过程更易于维护。

In this chapter, we are going to refactor our code to use the built-in middleware for global error handling to demonstrate the benefits of this approach. Since we already talked about the middleware in ASP.NET Core (in section 1.8), this section should be easier to understand.
在本章中,我们将重构我们的代码,以使用内置中间件进行全局错误处理,以演示这种方法的好处。由于我们已经在 ASP.NET Core 中讨论了中间件(在 1.8 节中),因此本节应该更容易理解。

5.1 Handling Errors Globally with the Built-In Middleware

5.1 使用内置中间件全局处理错误

The UseExceptionHandler middleware is a built-in middleware that we can use to handle exceptions. So, let’s dive into the code to see this middleware in action.‌
UseExceptionHandler 中间件是一个内置中间件,我们可以使用它来处理异常。因此,让我们深入研究代码,看看这个中间件的实际效果。

We are going to create a new ErrorModel folder in the Entities project, and add the new class ErrorDetails in that folder:
我们将在 Entities 项目中创建一个新的 ErrorModel 文件夹,并在该文件夹中添加新的类 ErrorDetails:

using System.Text.Json;

namespace Entities.ErrorModel
{
    public class ErrorDetails
    {
        public int StatusCode { get; set; }
        public string? Message { get; set; }
        public override string ToString() => JsonSerializer.Serialize(this);
    }
}

We are going to use this class for the details of our error message.
我们将使用这个类来获取错误消息的详细信息。

To continue, in the Extensions folder in the main project, we are going to add a new static class: ExceptionMiddlewareExtensions.cs.
要继续,在主项目的 Extensions 文件夹中,我们将添加新的静态类:ExceptionMiddlewareExtensions.cs。

Now, we need to modify it:
现在,我们需要修改它:

using Contracts;
using Entities.ErrorModel;
using Microsoft.AspNetCore.Diagnostics;
using System.Net;

namespace CompanyEmployees.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this WebApplication app, ILoggerManager logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    context.Response.ContentType = "application/json";
                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    if (contextFeature != null)
                    {
                        logger.LogError($"Something went wrong: {contextFeature.Error}");
                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Internal Server Error.",
                        }.ToString());
                    }
                });
            });
        }
    }
}

In the code above, we create an extension method, on top of the WebApplication type, and we call the UseExceptionHandler method. That method adds a middleware to the pipeline that will catch exceptions, log them, and re-execute the request in an alternate pipeline.
在上面的代码中,我们在 WebApplication 类型之上创建了一个扩展方法,并调用 UseExceptionHandler 方法。该方法将中间件添加到管道中,该中间件将捕获异常、记录异常,并在备用管道中重新执行请求。

Inside the UseExceptionHandler method, we use the appError variable of the IApplicationBuilder type. With that variable, we call the Run method, which adds a terminal middleware delegate to the application’s pipeline. This is something we already know from section 1.8.
在 UseExceptionHandler 方法中,我们使用 IApplicationBuilder 类型的 appError 变量。使用该变量,我们调用 Run 方法,该方法将终端中间件委托添加到应用程序的管道中。这是我们从 1.8 节中已经知道的。

Then, we populate the status code and the content type of our response, log the error message and finally return the response with the custom-created object. Later on, we are going to modify this middleware even more to support our business logic in a service layer.
然后,我们填充响应的状态代码和内容类型,记录错误消息,最后返回包含自定义创建对象的响应。稍后,我们将进一步修改这个中间件,以支持我们在服务层中的业务逻辑。

Of course, there are several namespaces we should add to make this work:
当然,我们应该添加几个命名空间才能实现此目的:

using Contracts;
using Entities.ErrorModel;
using Microsoft.AspNetCore.Diagnostics;
using System.Net;

5.2 Program Class Modification

5.2 Program类修改

To be able to use this extension method, let’s modify the Program class:‌
为了能够使用这个扩展方法,让我们修改 Program 类:

using CompanyEmployees.Extensions;
using Contracts;
using Microsoft.AspNetCore.HttpOverrides;
using NLog;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
LogManager.LoadConfiguration(
    string.Concat(Directory.GetCurrentDirectory(),
    "/nlog.config"));
builder.Services.ConfigureLoggerService();

builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();
builder.Services.ConfigureRepositoryManager();
builder.Services.ConfigureServiceManager();
builder.Services.ConfigureSqlContext(builder.Configuration);
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllers().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);

var app = builder.Build();

// Configure the HTTP request pipeline.
var logger = app.Services.GetRequiredService<ILoggerManager>();
app.ConfigureExceptionHandler(logger);

app.UseHttpsRedirection();

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.All
});

app.UseCors("CorsPolicy");
app.UseAuthorization();

app.MapControllers();

app.Run();

Here, we first extract the ILoggerManager service inside the logger variable. Then, we just call the ConfigureExceptionHandler method and pass that logger service. It is important to know that we have to extract the ILoggerManager service after the var app = builder.Build() code line because the Build method builds the WebApplication and registers all the services added with IOC.
在这里,我们首先提取 logger 变量中的 ILoggerManager 服务。然后,我们只需调用 ConfigureExceptionHandler 方法并传递该 Logger 服务。请务必知道,我们必须在 var app = builder.Build() 之后提取 ILoggerManager 服务,因为 Build 方法构建 WebApplication 并注册使用 IOC 添加的所有服务。

Additionally, we remove the call to the UseDeveloperExceptionPage method in the development environment since we don’t need it now and it also interferes with our error handler middleware.
此外,我们在开发环境中删除了对 UseDeveloperExceptionPage 方法的调用,因为我们现在不需要它,它还会干扰我们的错误处理程序中间件。

Finally, let’s remove the try-catch block from the GetAllCompanies service method:
最后,让我们从 GetAllCompanies 服务方法中删除 try-catch 块:

using AutoMapper;
using Contracts;
using Entities.Models;
using Service.Contracts;
using Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Service
{
    internal sealed class CompanyService : ICompanyService
    {
        private readonly IRepositoryManager _repository;
        private readonly ILoggerManager _logger;
        private readonly IMapper _mapper;
        public CompanyService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)
        {
            _repository = repository;
            _logger = logger;
            _mapper = mapper;
        }
        //public IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges)
        //{
        //    try
        //    {
        //        var companies = _repository.Company.GetAllCompanies(trackChanges);
        //        var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
        //        return companiesDto;
        //    }
        //    catch (Exception ex)
        //    {
        //        _logger.LogError($"Something went wrong in the {nameof(GetAllCompanies)} service method {ex}");
        //        throw;
        //    }
        //}

        public IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges)
        {
            var companies = _repository.Company.GetAllCompanies(trackChanges);
            var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
            return companiesDto;
        }

    }
}

And from our GetCompanies action:
从我们的 GetCompanies action中:

using Microsoft.AspNetCore.Mvc;
using Service.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CompanyEmployees.Presentation.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
        private readonly IServiceManager _service;
        public CompaniesController(IServiceManager service) => _service = service;

        //[HttpGet]
        //public IActionResult GetCompanies()
        //{
        //    try
        //    {
        //        var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
        //        return Ok(companies);
        //    }
        //    catch
        //    {
        //        return StatusCode(500, "Internal server error");
        //    }
        //}

        [HttpGet]
        public IActionResult GetCompanies()
        {
            var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
            return Ok(companies);
        }

    }
}

And there we go. Our methods are much cleaner now. More importantly, we can reuse this functionality to write more readable methods and actions in the future.
好了。我们现在的方法要干净得多。更重要的是,我们可以在未来重用此功能来编写更多可读的方法和作。

5.3 Testing the Result

5.3 测试结果

To inspect this functionality, let’s add the following line to the‌ GetCompanies action, just to simulate an error:
为了检查此功能,让我们将以下行添加到 GetCompanies作中,以模拟错误:

        [HttpGet]
        public IActionResult GetCompanies()
        {
            throw new Exception("Exception");
            var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
            return Ok(companies);
        }

NOTE: Once you send the request, Visual Studio will stop the execution inside the GetCompanies action on the line where we throw an exception. This is normal behavior and all you have to do is to click the continue button to finish the request flow. Additionally, you can start your app with CTRL+F5, which will prevent Visual Studio from stopping the execution. Also, if you want to start your app with F5 but still to avoid VS execution stoppages, you can open the Tools->Options->Debugging->General option and uncheck the Enable Just My Code checkbox.
注意:发送请求后,Visual Studio 将在引发异常的行上的 GetCompanies作中停止执行。这是正常行为,您所要做的就是单击 continue (继续) 按钮以完成请求流。此外,您可以使用 Ctrl+F5 启动应用程序,这将阻止 Visual Studio 停止执行。此外,如果您想使用 F5 启动应用程序,但仍要避免 VS 执行停止,则可以打开 Tools->Options->Debugging->General 选项,并取消选中 Enable Just My Code 复选框。

And send a request from Postman:
从 Postman发送请求:
https://localhost:5001/api/companies

We can check our log messages to make sure that logging is working as well.
我们可以检查日志消息以确保日志记录也正常工作。

Leave a Reply

Your email address will not be published. Required fields are marked *