系列导航

需求

因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在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应用(8)——实现全局异常处理的更多相关文章

  1. 使用.NET 6开发TodoList应用(3)——引入第三方日志库

    需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...

  2. 使用.NET 6开发TodoList应用(1)——系列背景

    前言 想到要写这样一个系列博客,初衷有两个:一是希望通过一个实践项目,将.NET 6 WebAPI开发的基础知识串联起来,帮助那些想要入门.NET 6服务端开发的朋友们快速上手,对使用.NET 6开发 ...

  3. 使用.NET 6开发TodoList应用(2)——项目结构搭建

    为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...

  4. 使用.NET 6开发TodoList应用(4)——引入数据存储

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其 ...

  5. 使用.NET 6开发TodoList应用(5)——领域实体创建

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文 ...

  6. 使用.NET 6开发TodoList应用(5.1)——实现Repository模式

    需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是 ...

  7. 使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求

    需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实 ...

  8. 使用.NET 6开发TodoList应用文章索引

    系列导航 使用.NET 6开发TodoList应用(1)--系列背景 使用.NET 6开发TodoList应用(2)--项目结构搭建 使用.NET 6开发TodoList应用(3)--引入第三方日志 ...

  9. 使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求

    系列导航 使用.NET 6开发TodoList应用文章索引 需求 需求很简单:实现GET请求获取业务数据.在这个阶段我们经常使用的类库是AutoMapper. 目标 合理组织并使用AutoMapper ...

随机推荐

  1. 系统发育树邻接法(NJ)和非加权组平均法(UPGMA)之比较

    目录 1.原理的区别 2.实操比较 UPGMA NJ法 保存树文件 更深理解 1.原理的区别 主要区别在于,非加权组平均法(UPGMA)是基于平均链接方法的聚集层次聚类方法,而邻接法(NJ)是基于最小 ...

  2. python16线程

    python对于I/O密集型应用比较好,具体根据是什么类型应用来查看 对于cpu密集型应用可以借助python的一些扩展去实现 thread模块是比较早期的模块,thresding是比较新的模块,对t ...

  3. Google服务器架构图解简析

    无疑是互联网时代最闪亮的明星.截止到今天为止,Google美国主站在Alexa排名已经连续3年第一,Alexa Top100中,各国的Google分站竟然霸占了超过20多个名额,不得不令人感叹Goog ...

  4. UE4之Slate:App默认窗口的创建流程

    UE4版本:4.24.3源码编译 Windows10 + VS2019开发环境 在先前分享的基础上,现在来梳理下App启动时默认窗口的创建流程,以及相关的类.对象之间的抽象层级: 纯C++工程配置 S ...

  5. dubbo 协议的 K8s pod 存活探针配置

    背景 某项目采用微服务架构,dubbo 框架,K8s 方式部署. 其中 HTTP 协议由网关应用统一处理,大部分应用仅提供 dubbo 协议. 目标 应用某个实例(pod)状态异常时,尝试自动重启恢复 ...

  6. 面向Web应用的并发压力测试工具——Locust实用攻略

    1. 概述 该方案写作目的在于描述一个基于Locust实现的压力测试,文中详细地描述了如何利用locustfile.py文件定义期望达成的测试用例,并利用Locust对目标站点进行并发压力测试. 特别 ...

  7. day08 外键字段的增删查改

    day08 外键字段的增删查改 今日内容概要 外键字段的增删查改 正反向查询的概念 基于对象的跨表查询(子查询) 基于双下划线的跨表查询(连表操作) 聚合查询与分组查询 F查询和Q查询 前提准备 cl ...

  8. 26. Linux GIT

    windows git 下载链接: Msysgit   https://git-scm.com/download/win 1 进入git bash进行第一次配置 git config --global ...

  9. Learning Spark中文版--第六章--Spark高级编程(1)

    Introduction(介绍) 本章介绍了之前章节没有涵盖的高级Spark编程特性.我们介绍两种类型的共享变量:用来聚合信息的累加器和能有效分配较大值的广播变量.基于对RDD现有的transform ...

  10. PS只能各个工具使用的注意知识点

    1.图章工具  <仿制图章工具>使用方法:按住alt点击吸取干净的地方,然后松开alt键,按住鼠标左键拖动或左击  擦拭 图章区域放大缩小,是按住alt键+鼠标右键左右滑动 当图片中多个图 ...