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

public class ResultModelBase
{
    public int? Code { get; set; }
}

DataResultModel 類用屬性 Data 來包裝返回結果

public class DataResultModel:ResultModelBase
{
    public DataResultModel(object data,int? code = 200)
    {
        Code = code;
        Data = data;
    }
    
    public object Data { get; set; }
}

MessageResultModel 類使用屬性 Message 類返回消息文本

public class MessageResultModel:ResultModelBase
{
    public MessageResultModel(string massage,int? code = 200)
    {
        Code = code;
        Message = massage;
    }

    public string Message { get; set; }
}

CustomExceptionResultModel 類中可以傳入 Exception 類型和定義一些其他的相關屬性

public class CustomExceptionResultModel:ResultModelBase
{
    public CustomExceptionResultModel(Exception exception,int? code = 500)
    {
        Code = code;
        Reason = exception.InnerException != null ?
            exception.InnerException.Message :
            exception.Message;
    }

    public string Reason { get; set; }
}

DataResult、MessageResult 和 CustomExceptionResult 類都是繼承自ObjectResult,將相對應的 Model 類包裝後通過構造函數賦值給 ObjectResult 的 Value 屬性,用於最後的結果返回。

public class DataResult: ObjectResult
{
    public DataResult(object data , int? code=200 )
        : base(new DataResultModel(data,code))
    {
        StatusCode = 200;
    }
}
public class MessageResult:ObjectResult
{
    public MessageResult(string message, int? code=200 )
        : base(new MessageResultModel(message,code))
    {
        StatusCode = 200;
    }
}
public class CustomExceptionResult:ObjectResult
{
    public CustomExceptionResult(Exception exception,HttpStatusCode statusCode,  int? code=500 )
        : base(new CustomExceptionResultModel(exception,code))
    {
        StatusCode = (int)statusCode;
    }
}

使用兩個過濾器對返回結果進行處理

public class CustomerExceptionAttribute: IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        HttpStatusCode status = HttpStatusCode.InternalServerError;

        int code = (int) status;
        //處理各種異常
        if (context.Exception is UserNotFoundException)
        {
            code = 500001;
        }
        context.Result = new CustomExceptionResult(context.Exception,status ,code);
        context.ExceptionHandled = true;
    }
}

public class ResultFilterAttribute:ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var objectResult = context.Result as ObjectResult;
        if (objectResult?.Value == null)
        {
            context.Result=new NotFoundObjectResult(new MessageResult("未找到資源"));
        }
        
        if (context.Result is MessageResult)
        {
            context.Result = new MessageResult(objectResult.Value.ToString());
        }
        else if (context.Result is OkObjectResult || context.Result is ObjectResult)
        {
            context.Result = new DataResult(objectResult.Value);
        }
    }
}

用戶添加接口

在 UserRepository 中添加 AddUser 方法

public User AddUser(User user)
{
    int id=_users.OrderByDescending(x => x.Id).First().Id + 1;
    user.Id = id;
    _users.Add(user);

    return user;
}

示例中沒有實際操作數據庫,_users 是一個 List 對象,當 _users 爲 Null 或內容爲空時,_users.OrderByDescending(x => x.Id).First() 的執行就會報錯,空對象的問題在實際程序中無處不在,修改後的代碼如下:

public User AddUser(User user)
{
    int id = 1;
    if (_users.Any())
    {
        id=_users.OrderByDescending(x => x.Id).First().Id + 1;
    }
    user.Id = id;
    _users.Add(user);

    return user;
}

在 Controller 層的 AddUser 方法也需要對入參實體進行檢查

[HttpPost]
public User AddUser(User user)
{
    return _userService.AddUser(user);
}

public class User
{
    public int Id { get; set; }

    [Required(ErrorMessage = "用戶名不能爲空")]

    public string Name { get; set; }
    [Required(ErrorMessage = "用戶編碼不能爲空")]
    public string Code { get; set; }
}

實際情況下接口層的入參實體和底層的數據實體需要分開,然後使用 AutoMapper 之類的映射工具進行轉換,本示例中使用了同一個 User 。

使用 Postman 進行調用,當 Name 或 Code 爲空時,結果如下:

默認的返回結果格式和上面定義的統一的格式有些區別,大家可以思考下,怎樣使用過濾器的方式將參數驗證的返回信息進行統一輸出。

根據 Id 獲取用戶的名稱

在 UserRepository 中有根據 Id 獲取 User 對象的方法

public User GetUserById(int id)
{
    return _users.Find(x => x.Id == id);
}

在 UserService 中添加 GetUserName 方法獲取名稱

public string GetUserName(int id)
{
    User user=_userRepository.GetUserById(id);
    if (user == null)
    {
        throw new UserNotFoundException($"用戶id:{id} 在數據庫不存在" );
    }
    return user.Name;
}

當通過 id 找不到 User 對象時,可以拋出 UserNotFoundException 異常,如果只是對 user 對象進行 Null 判斷然後返回一個空字符,就弄不清楚是 user 對象不存在還是用戶名爲空。

獲取用戶全名

下面用一個獲取用戶全名(包含部門)的業務來模擬異常的重新包裝,部門操作的相關類就不在贅述了,可以在文章最下方的鏈接中查看源碼。

UserController 中添加了接口方法

[HttpGet("{id}")]
public string GetFullName(int id)
{
    return _userService.GetFullName(id);
}

UserService 中添加 GetFullName 方法

public string GetFullName(int id)
{
    try
    {
        User user = GetUserById(id);
        string deptName = _deptService.GetDeptName(user.ParentId);
        //處理其他邏輯
        return $"{user.Name}[{deptName}]";
    }
    catch (Exception e)
    {
        throw new UserFullNameGenException($"用戶 Id 爲 {id} 的 FullName 生產失敗",e);
    }
}
  • GetUserById 方法和 _deptService.GetDeptName 方法中都可能拋異常,在上次可以捕獲異常然後拋出符合當前業務的 UserFullNameGenException 異常;
  • 捕獲的異常 e 作爲 UserFullNameGenException 異常的 InnerException 傳入,這樣如果層級比較多,通過 InnerException 就可以追溯到最底層的原因。

當輸入參數爲用戶不存在的時候調用結果如下:

當輸入參數爲用戶的部門不存在時調用結果如下:

  • 通過二次捕獲提示的錯誤信息是跟當前業務有關的,可以更容易定位問題,更底一層的原因可以在 InnerException 中獲取;
  • 兩次異常是不同原因造成的,但對於這個業務來說就是獲取 FullName 失敗,返回的錯誤碼也是一致的 500100 ;
  • 因爲有了二次捕獲,異常堆棧信息中只能定位到最上層捕獲異常的地方,如果需要知道更底層的異常堆棧,可以將 InnerException 的堆棧信息進行合併。

最後

本文以一個簡單的示例演示了代碼中異常的處理,但重要的不是編碼而是處理問題的思路。具體應該怎麼做還是需要結合當前的上下文。希望本文對您有所幫助。

示例源碼:https://github.com/oec2003/DotNetCoreThreeAPIDemo/tree/master/ExceptionDemo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章