dotNET:怎样处理程序中的异常(实战篇)?
在上篇 《dotNET:怎样处理程序中的异常(理论篇)》 中讲了一些程序中出现异常怎样处理的理论知识,本文将以代码的方式来进行实践。
环境
- dotNET Core:3.1
- 工具:Rider 2019.3.2
- 系统:macOS 10.15.4
创建项目
在 Rider 中创建示例项目 ExceptionDemo ,该项目为 dotNET Core 3.1 的 WebAPI 项目,为了演示方便,不同层级以目录的方式放在了一个项目中,创建好的项目目录结构如下:
- Controllers
- UserController:操作用户的控制器
- CustomExceptions
- UserNotFoundException:用户不存在的自定义异常类
- Filters
- CustomerExceptionAttribute:异常结果处理过滤器
- ResultFilterAttribute:普通结果处理过滤器
- Models
- CustomExceptionResult:异常返回的处理类
- CustomExceptionResultModel:异常内容的模型类
- DataResult:普通结果的返回处理类
- DataResultModel:普通结果的内容模型类
- MessageResult:消息结果的返回处理类
- MessageResultModel:消息结果的内容模型类
- ResultModelBase:返回结果内容模型的基类
- User:示例中用户的实体类
- Repositories
- IUserRepository:用户操作数据库的接口
- UserRepository:用户操作数据库的实现类
- Services
- IUserService:用户业务层的接口
- UserService:用户业务层的实现类
结果的返回
接口的返回可以归纳为三种情况:
- 正常的请求数据的返回
- 通过判断需要返回一些消息给前端进行提示
- 异常的返回
所以上面定义了 DataResult、MessageResult 和 CustomExceptionResult 相关类来进行这三种情况的封装。
这三个类都继承 ResultModelBase 类,ResultModelBase 类中只定义了 Code
1 | public class ResultModelBase |
DataResultModel 类用属性 Data 来包装返回结果
1 | public class DataResultModel:ResultModelBase |
MessageResultModel 类使用属性 Message 类返回消息文本
1 | public class MessageResultModel:ResultModelBase |
CustomExceptionResultModel 类中可以传入 Exception 类型和定义一些其他的相关属性
1 | public class CustomExceptionResultModel:ResultModelBase |
DataResult、MessageResult 和 CustomExceptionResult 类都是继承自ObjectResult,将相对应的 Model 类包装后通过构造函数赋值给 ObjectResult 的 Value 属性,用于最后的结果返回。
1 | public class DataResult: ObjectResult |
使用两个过滤器对返回结果进行处理
1 | public class CustomerExceptionAttribute: IExceptionFilter |
用户添加接口
在 UserRepository 中添加 AddUser 方法
1 | public User AddUser(User user) |
示例中没有实际操作数据库,_users 是一个 List_users.OrderByDescending(x => x.Id).First()
的执行就会报错,空对象的问题在实际程序中无处不在,修改后的代码如下:
1 | public User AddUser(User user) |
在 Controller 层的 AddUser 方法也需要对入参实体进行检查
1 | [HttpPost] |
实际情况下接口层的入参实体和底层的数据实体需要分开,然后使用 AutoMapper 之类的映射工具进行转换,本示例中使用了同一个 User 。
使用 Postman 进行调用,当 Name 或 Code 为空时,结果如下:
默认的返回结果格式和上面定义的统一的格式有些区别,大家可以思考下,怎样使用过滤器的方式将参数验证的返回信息进行统一输出。
根据 Id 获取用户的名称
在 UserRepository 中有根据 Id 获取 User 对象的方法
1 | public User GetUserById(int id) |
在 UserService 中添加 GetUserName 方法获取名称
1 | public string GetUserName(int id) |
当通过 id 找不到 User 对象时,可以抛出 UserNotFoundException 异常,如果只是对 user 对象进行 Null 判断然后返回一个空字符,就弄不清楚是 user 对象不存在还是用户名为空。
获取用户全名
下面用一个获取用户全名(包含部门)的业务来模拟异常的重新包装,部门操作的相关类就不在赘述了,可以在文章最下方的链接中查看源码。
UserController 中添加了接口方法
1 | [HttpGet("{id}")] |
UserService 中添加 GetFullName 方法
1 | public string GetFullName(int id) |
- GetUserById 方法和 _deptService.GetDeptName 方法中都可能抛异常,在上次可以捕获异常然后抛出符合当前业务的 UserFullNameGenException 异常;
- 捕获的异常 e 作为 UserFullNameGenException 异常的 InnerException 传入,这样如果层级比较多,通过 InnerException 就可以追溯到最底层的原因。
当输入参数为用户不存在的时候调用结果如下:
当输入参数为用户的部门不存在时调用结果如下:
- 通过二次捕获提示的错误信息是跟当前业务有关的,可以更容易定位问题,更底一层的原因可以在 InnerException 中获取;
- 两次异常是不同原因造成的,但对于这个业务来说就是获取 FullName 失败,返回的错误码也是一致的 500100 ;
- 因为有了二次捕获,异常堆栈信息中只能定位到最上层捕获异常的地方,如果需要知道更底层的异常堆栈,可以将 InnerException 的堆栈信息进行合并。
最后
本文以一个简单的示例演示了代码中异常的处理,但重要的不是编码而是处理问题的思路。具体应该怎么做还是需要结合当前的上下文。希望本文对您有所帮助。
示例源码:https://github.com/oec2003/DotNetCoreThreeAPIDemo/tree/master/ExceptionDemo