使用.NET 6开发TodoList应用(8)——实现全局异常处理
系列导航
需求
因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在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
请求。
参考资料
使用.NET 6开发TodoList应用(8)——实现全局异常处理的更多相关文章
- 使用.NET 6开发TodoList应用(3)——引入第三方日志库
需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...
- 使用.NET 6开发TodoList应用(1)——系列背景
前言 想到要写这样一个系列博客,初衷有两个:一是希望通过一个实践项目,将.NET 6 WebAPI开发的基础知识串联起来,帮助那些想要入门.NET 6服务端开发的朋友们快速上手,对使用.NET 6开发 ...
- 使用.NET 6开发TodoList应用(2)——项目结构搭建
为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...
- 使用.NET 6开发TodoList应用(4)——引入数据存储
需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其 ...
- 使用.NET 6开发TodoList应用(5)——领域实体创建
需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文 ...
- 使用.NET 6开发TodoList应用(5.1)——实现Repository模式
需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是 ...
- 使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求
需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实 ...
- 使用.NET 6开发TodoList应用文章索引
系列导航 使用.NET 6开发TodoList应用(1)--系列背景 使用.NET 6开发TodoList应用(2)--项目结构搭建 使用.NET 6开发TodoList应用(3)--引入第三方日志 ...
- 使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求
系列导航 使用.NET 6开发TodoList应用文章索引 需求 需求很简单:实现GET请求获取业务数据.在这个阶段我们经常使用的类库是AutoMapper. 目标 合理组织并使用AutoMapper ...
随机推荐
- Redis list操作命令
rpop命令 用于移除列表的最后一个元素,返回值为移除的元素.当列表不存在时,返回nil. 基本语法: rpop key_name LPOP:移除并返回列表第一个元素 RPOP:移除并返回列表最后一个 ...
- IDEA 注释模板配置
配置创建类的注释模板 Ctrl + Shift + a --> File and Code Templates // 快捷定位配置 // 路径:File --> Settings --&g ...
- 比对软件Blast,Blast+,Diamond比较
1. Blast (1)格式化数据库 formatdb -i db.seq -p T -o T -l logfile 主要参数: -i 输入需要格式化的源数据库名称 -p 文件类型,是核苷酸序列数据库 ...
- perl和python3 同时打开两个文件
perl : while(defined(my $f1=<FQ1>) && defined(my $f2=<FQ2>)){ condition } python ...
- 用Rsync实现windows下同步linux服务器的数据
一:环境 1.服务端:Red Hat Enterprise Linux Server release 6.4 (Santiago) 2.客户端:windows7旗舰版64位 3.同步对象:测试数据 4 ...
- Python中类的相关介绍
本文主要介绍python中类的概念性内容,如类的定义.说明及简单使用 1. 类的简单介绍 1 # -*- coding:utf-8 -*- 2 # Author:Wong Du 3 4 ''' 5 - ...
- A Child's History of England.44
At this period of his reign, when his troubles seemed so few and his prospects so bright, those dome ...
- Hbase(6)【Java Api Phoenix操作Hbase】
目录 两种方式操作Phoenix 1.Thick Driver 2.Thin Driver 3.注意事项 两种方式操作Phoenix 官网:http://phoenix.apache.org/faq. ...
- Hive(四)【DML 数据导入导出】
目录 一.数据导入 1.1 [load]--向数据中装载数据 案例 1.2 [insert]--查询语句向表中插入数据 案例 1.3 [as select]--查询语句中创建表且加载数据 案例 1.4 ...
- VIM中把^M替换为真正的换行符
:%s/\r/\r/g 或者:%s/^M/\r/g 红色的^M不是直接打出,而是按住ctrl再依次按下V和M