.NET 6开发TodoList应用之实现全局异常处理

目录
  • 需求
  • 目标
  • 原理和思路
  • 实现
  • 验证
  • 总结
  • 参考资料

需求

因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在Controller里就需要进行完整的try-catch捕获,并根据是否有异常抛出重新包装返回值。这是一项机械且繁琐的工作。有没有办法让框架自己去做这件事呢?

有的,解决方案的名称叫做全局异常处理,或者叫做如何让接口优雅地失败。

目标

我们希望将异常处理和消息返回放到框架中进行统一处理,摆脱Controller层的try-catch块。

原理和思路

一般而言用来实现全局异常处理的思路有两种,但是出发点都是通过.NET Web API的管道中间件Middleware Pipeline实现的。第一种方式是通过.NET内建的中间件来实现;第二种是完全自定义中间件实现。

我们会简单地介绍一下如何通过内建中间件实现,然后实际使用第二种方式来实现我们的代码,大家可以比较一下异同。

Api项目中创建Models文件夹并创建ErrorResponse类。

ErrorResponse.cs

using System.Net;
using System.Text.Json;

namespace TodoList.Api.Models;

public class ErrorResponse
{
    public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.InternalServerError;
    public string Message { get; set; } = "An unexpected error occurred.";
    public string ToJsonString() => JsonSerializer.Serialize(this);
}

创建Extensions文件夹并新建一个静态类ExceptionMiddlewareExtensions实现一个静态扩展方法:

ExceptionMiddlewareExtensions.cs

using System.Net;
using Microsoft.AspNetCore.Diagnostics;
using TodoList.Api.Models;

namespace TodoList.Api.Extensions;

public static class ExceptionMiddlewareExtensions
{
    public static void UseGlobalExceptionHandler(this WebApplication app)
    {
        app.UseExceptionHandler(appError =>
        {
            appError.Run(async context =>
            {
                context.Response.ContentType = "application/json";

                var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
                if (errorFeature != null)
                {
                    await context.Response.WriteAsync(new ErrorResponse
                    {
                        StatusCode = (HttpStatusCode)context.Response.StatusCode,
                        Message = errorFeature.Error.Message
                    }.ToJsonString());
                }
            });
        });
    }
}

在中间件配置的最开始配置好,注意中间件管道是有顺序的,把全局异常处理放到第一步(同时也是请求返回的最后一步)能确保它能拦截到所有可能发生的异常。即这个位置:

var app = builder.Build();
app.UseGlobalExceptionHandler();

就可以实现全局异常处理了。接下来我们看如何完全自定义一个全局异常处理的中间件,其实原理是完全一样的,只不过我更偏向自定义中间件的代码组织方式,更加简洁和一目了然。

与此同时,我们希望对返回值进行格式上的统一包装,于是定义了这样的返回类型:

ApiResponse.cs

using System.Text.Json;

namespace TodoList.Api.Models;

public class ApiResponse<T>
{
    public T Data { get; set; }
    public bool Succeeded { get; set; }
    public string Message { get; set; }

    public static ApiResponse<T> Fail(string errorMessage) => new() { Succeeded = false, Message = errorMessage };
    public static ApiResponse<T> Success(T data) => new() { Succeeded = true, Data = data };

    public string ToJsonString() => JsonSerializer.Serialize(this);
}

实现

在Api项目中新建Middlewares文件夹并新建中间件GlobalExceptionMiddleware

GlobalExceptionMiddleware.cs

using System.Net;
using TodoList.Api.Models;

namespace TodoList.Api.Middlewares;

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;

    public GlobalExceptionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            // 你可以在这里进行相关的日志记录
            await HandleExceptionAsync(context, exception);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = exception switch
        {
            ApplicationException => (int)HttpStatusCode.BadRequest,
            KeyNotFoundException => (int)HttpStatusCode.NotFound,
            _ => (int)HttpStatusCode.InternalServerError
        };

        var responseModel = ApiResponse<string>.Fail(exception.Message);

        await context.Response.WriteAsync(responseModel.ToJsonString());
    }
}

这样我们的ExceptionMiddlewareExtensions就可以写成下面这样了:

ExceptionMiddlewareExtensions.cs

using TodoList.Api.Middlewares;

namespace TodoList.Api.Extensions;

public static class ExceptionMiddlewareExtensions
{
    public static WebApplication UseGlobalExceptionHandler(this WebApplication app)
    {
        app.UseMiddleware<GlobalExceptionMiddleware>();
        return app;
    }
}

验证

首先我们需要在Controller中包装我们的返回值,举一个CreateTodoList的例子,其他的类似修改:

TodoListController.cs

[HttpPost]
public async Task<ApiResponse<Domain.Entities.TodoList>> Create([FromBody] CreateTodoListCommand command)
{
    return ApiResponse<Domain.Entities.TodoList>.Success(await _mediator.Send(command));
}

还记得我们在TodoList的领域实体上有一个Colour的属性吗,它是一个值对象,并且在赋值的过程中我们让它有机会抛出一个UnsupportedColourException,我们就用这个领域异常来验证全局异常处理。

为了验证需要,我们可以对CreateTodoListCommand做一些修改,让它接受一个Colour的字符串,相应修改如下:

CreateTodoListCommand.cs

public class CreateTodoListCommand : IRequest<Domain.Entities.TodoList>
{
    public string? Title { get; set; }
    public string? Colour { get; set; }
}

// 以下代码位于对应的Handler中,省略其他...
var entity = new Domain.Entities.TodoList
{
    Title = request.Title,
    Colour = Colour.From(request.Colour ?? string.Empty)
};

启动Api项目,我们试图以一个不支持的颜色来创建TodoList:

请求

响应

顺便去看下正常返回的格式是否按我们预期的返回,下面是请求所有TodoList集合的接口返回:

可以看到正常和异常的返回类型已经统一了。

总结

其实实现全局异常处理还有一种方法是通过Filter来做,具体方法可以参考这篇文章:Filters in ASP.NET Core,我们之所以不选择Filter而使用Middleware主要是基于简单、易懂,并且作为中间件管道的第一个个中间件加入,有效地覆盖包括中间件在内的所有组件处理过程。Filter的位置是在路由中间件作用之后才被调用到。实际使用中,两种方式都有应用。

下一篇我们来实现PUT请求。

参考资料

1.Write custom ASP.NET Core middleware

2.Filters in ASP.NET Core 

到此这篇关于.NET 6开发TodoList应用之实现全局异常处理的文章就介绍到这了,更多相关.NET 6 全局异常处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • .NET 6开发TodoList应用引入数据存储

    目录 一.需求 二.目标 三.原理和思路 四.实现 1. 引入Nuget包并进行配置 2. 添加DBContext对象并进行配置# 3. 配置文件修改 4. 主程序配置 5. 本地运行MSSQL Server容器及数据持久化 五.验证 一.需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储. 目前的需求很简单: 需要能持久化TodoList对象并对其进行操作: 需要能持久化TodoItem对象并对其进行操作

  • .NET 6开发TodoList应用实现结构搭建

    目录 1.TodoList需求简介 2.开发工具 2.1.NET 6 2.2Visual Studio Code 2.3Hoppscotch 3.Clean Architecture简介 4.搭建解决方案结构 5.运行 往期学习: .NET 6开发TodoList应用实现系列背景 1.TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能,我不会一次性把所有的特性诸如允许用户登陆之类的需求全部写上,只是先列出最基本的功能性需求: 我们可以维护一个TodoL

  • .NET 6开发TodoList应用之使用MediatR实现POST请求

    目录 需求 目标 原理与思路 CQRS模式 中介者Mediator模式 MediatR 实现 引入MediatR 实现Post请求 验证 创建TodoList验证 创建TodoItem验证 总结 参考资料 需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实体对象:调用IRepository<T>完成数据库的写入,最多会在中间加一层Service.

  • .NET 6开发TodoList应用之实现Repository模式

    目录 需求 目标 原理和思路 实现 通用Repository实现 引入使用 验证 总结 需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是开始思考是否可以将这些操作抽象出去,当然是可以的,而且被抽象出去的部分是可以不加改变地在今后的任何有此需求的项目中直接引入使用. 那么我们本文的需求就是:如何实现一个可重用的Repository模块. 长文预警,

  • .NET 6开发TodoList应用实现系列背景

    目录 1.列说明 2.系列导航 2.1 使用.NET 6开发TodoList应用文章索引 2.1.1创建项目 2.1.2.NET 6 WebAPI Program.cs的变更 2.1.3Change 1: Top-level statements 2.1.4Change 2: Implicit using directives 2.1.5Change 3: No Startup class 2.2 关于Pipeline的一些知识点 2.2.1Pipeline Sequence 2.2.2app.

  • .NET 6开发TodoList应用之使用AutoMapper实现GET请求

    目录 需求 目标 原理与思路 实现 引入AutoMapper 实现GET请求 验证 获取所有TodoList列表 获取单个TodoList详情 填一个POST文章里的坑 总结 需求 需求很简单:实现GET请求获取业务数据.在这个阶段我们经常使用的类库是AutoMapper. 目标 合理组织并使用AutoMapper,完成GET请求. 原理与思路 首先来简单地介绍一下这这个类库. 关于AutoMapper 在业务侧代码和数据库实体打交道的过程中,一个必不可少的部分就是返回的数据类型转换.对于不同的

  • .NET 6开发TodoList应用引入第三方日志库

    目录 1.需求 2.目标 3.原理和思路 4.实现 4.1日志配置实现 4.2主程序配置 4.3注入使用 5.验证 1.需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选择对应平台的日志SDK进行集成.使用Serilog提供的多种Sink,可以实现将日志写入不同云平台或者是非云平台的日志存储中去,这是我们这篇文章讲要研究的内容. 2.目标 我们将为

  • 使用.NET 6开发TodoList应用之领域实体创建原理和思路

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文预警!包含大量代码 目标 在本文中,我们希望达到以下几个目标: 定义领域实体: 通过数据库操作领域实体: 原理和思路 虽然TodoList是一个很简单的应用,业务逻辑并不复杂,至少在这个系列文章中我并不想使其过度复杂.但是我还是打算借此简单地涉及领域驱动开发(DDD)的基础概念. 首先比较明确的是,

  • .NET 6开发TodoList应用之实现全局异常处理

    目录 需求 目标 原理和思路 实现 验证 总结 参考资料 需求 因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在Controller里就需要进行完整的try-catch捕获,并根据是否有异常抛出重新包装返回值.这是一项机械且繁琐的工作.有没有办法让框架自己去做这件事呢? 有的,解决方案的名称叫做全局异常处理,或者叫做如何让接口优雅地失败. 目标 我们希望将异常处理和消息返回放到框架中进行统一处理,摆脱Controller层的try-catch块. 原理和思路 一般而言用来实现全局异

  • 详解Spring Boot2 Webflux的全局异常处理

    本文首先将会回顾Spring 5之前的SpringMVC异常处理机制,然后主要讲解Spring Boot 2 Webflux的全局异常处理机制. SpringMVC的异常处理 Spring 统一异常处理有 3 种方式,分别为: 使用 @ExceptionHandler 注解 实现 HandlerExceptionResolver 接口 使用 @controlleradvice 注解 使用 @ExceptionHandler 注解 用于局部方法捕获,与抛出异常的方法处于同一个Controller类

  • .NET 6开发TodoList应用之实现DELETE请求与HTTP请求幂等性

    目录 需求 目标 原理与思路 实现 验证 总结 需求 先说明一下关于原本想要去更新的PATCH请求的文章,从目前试验的情况来看,如果是按照.NET 6的项目结构(即只使用一个Program.cs完成程序初始化),那微软官方给出的文档目前还没有对应地更新,按照之前的方式进行JsonPatch的配置是不行的,目前已经有人在Github微软的官方文档Repo下提了ISSUE: .NET 6: JsonPatch in ASP.NET Core web API.并且因为PATCH的使用频率并不高,所以我

  • .NET 6开发TodoList应用之实现ActionFilter

    目录 需求 目标 原理与思路 实现 验证 总结 需求 Filter在.NET Web API项目开发中也是很重要的一个概念,它运行在执行MVC响应的Pipeline中执行,允许我们将一些可以在多个Action之间重用的逻辑抽取出来集中管理.虽然我们在上一篇使用.NET 6开发TodoList应用之实现接口请求验证中演示了如何通过使用MediatR提供的IPipelineBehavior接口在CQRS的Handle方法执行前后插入可重用代码,而本文所演示的Filters作用在Controller的

  • .NET 6开发TodoList应用实现系列背景

    目录 1.列说明 2.系列导航 2.1 使用.NET 6开发TodoList应用文章索引 2.1.1创建项目 2.1.2.NET 6 WebAPI Program.cs的变更 2.1.3Change 1: Top-level statements 2.1.4Change 2: Implicit using directives 2.1.5Change 3: No Startup class 2.2 关于Pipeline的一些知识点 2.2.1Pipeline Sequence 2.2.2app.

  • 使用.NET 6开发TodoList应用之引入数据存储的思路详解

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其进行操作: 需要能持久化TodoItem对象并对其进行操作: 问题是,我们打算如何存储数据? 存储组件的选择非常多:以MSSQL Server/Postgres/MySql/SQLite等为代表的关系型数据库,以MongoDB/ElasticSearch等为代表的非关系型数据库,除此之外,我们还可以

随机推荐