網站首頁 編程語言 正文
前言
近期在重新搭建一套基于ASP.NET Core WebAPI的框架,這其中確實帶來了不少的收獲,畢竟當你想搭建一套框架的時候,你總會不自覺的去想,如何讓這套框架變得更完善一點更好用一點。其中在關于WebApi統一結果返回的時候,讓我也有了更一步的思考,首先是如何能更好的限制返回統一的格式,其次是關于結果的包裝一定是更簡單更強大。在不斷的思考和完善中,終于有了初步的成果,便分享出來,學無止境思考便無止境,希望以此能與君共勉。
統一結果類封裝
首先如果讓返回的結果格式統一,就得有一個統一的包裝類去包裝所有的返回結果,因為返回的具體數據雖然格式一致,但是具體的值的類型是不確定的,因此我們這里需要定義個泛型類。當然如果你不選擇泛型類的話用dynamic或者object類型也是可以的,但是這樣的話可能會帶來兩點不足
- 一是可能會存在裝箱拆箱的操作。
- 二是如果引入swagger的話是沒辦法生成返回的類型的,因為dynamic或object類型都是執行具體的action時才能確定返回類型的,但是swagger的結構是首次運行的時候就獲取到的,因此無法感知具體類型。
定義包裝類
上面我們也說了關于定義泛型類的優勢,這里就話不多說來直接封裝一個結果返回的包裝類
public class ResponseResult{ /// /// 狀態結果 /// public ResultStatus Status { get; set; } = ResultStatus.Success; private string? _msg; ////// 消息描述 /// public string? Message { get { // 如果沒有自定義的結果描述,則可以獲取當前狀態的描述 return !string.IsNullOrEmpty(_msg) ? _msg : EnumHelper.GetDescription(Status); } set { _msg = value; } } ////// 返回結果 /// public T Data { get; set; } }
其中這里的ResultStatus
是一個枚舉類型,用于定義具體的返回狀態碼,用于判斷返回的結果是正常還是異?;蛘咂渌?,我這里只是簡單的定義了一個最簡單的示例,有需要的話也可以自行擴展
public enum ResultStatus { [Description("請求成功")] Success = 1, [Description("請求失敗")] Fail = 0, [Description("請求異常")] Error = -1 }
這種情況下定義枚舉類型并且結合它的DescriptionAttribute
的特性去描述枚舉的含義是一個不錯的選擇,首先它可以統一管理每個狀態的含義,其次是更方便的獲取每個狀態對應的描述。這樣的話如果沒有自定義的結果描述,則可以獲取當前狀態的描述來充當默認值的情況。這個時候在寫具體action的時候會是以下的效果
[HttpGet("GetWeatherForecast")] public ResponseResult> GetAll() { var datas = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }); return new ResponseResult > { Data = datas }; }
這樣的話每次編寫action的時候都可以返回一個ResponseResult
的結果了,這里就體現出了使用枚舉定義狀態碼的優勢了,相當一部分場景我們可以省略了狀態碼甚至是消息的編寫,畢竟很多時候在保障功能的情況下,代碼還是越簡介越好的,更何況是一些高頻操作呢。
升級一下操作
上面雖然我們定義了ResponseResult
來統一包裝返回結果,但是每次還得new一下,在無疑是不太方便的,而且還要每次都還得給屬性賦值啥的,也是夠麻煩的,這個時候就想,如果能有相關的輔助方法去簡化操作就好了,方法不用太多能滿足場景就好,也就是夠用就好,最主要的是能支持擴展就可以。因此,進一步升級一下結果包裝類,來簡化一下操作
public class ResponseResult{ /// /// 狀態結果 /// public ResultStatus Status { get; set; } = ResultStatus.Success; private string? _msg; ////// 消息描述 /// public string? Message { get { return !string.IsNullOrEmpty(_msg) ? _msg : EnumHelper.GetDescription(Status); } set { _msg = value; } } ////// 返回結果 /// public T Data { get; set; } ////// 成功狀態返回結果 /// /// 返回的數據 ///public static ResponseResult SuccessResult(T data) { return new ResponseResult { Status = ResultStatus.Success, Data = data }; } /// /// 失敗狀態返回結果 /// /// 狀態碼 /// 失敗信息 ///public static ResponseResult FailResult(string? msg = null) { return new ResponseResult { Status = ResultStatus.Fail, Message = msg }; } /// /// 異常狀態返回結果 /// /// 狀態碼 /// 異常信息 ///public static ResponseResult ErrorResult(string? msg = null) { return new ResponseResult { Status = ResultStatus.Error, Message = msg }; } /// /// 自定義狀態返回結果 /// /// /// ///public static ResponseResult Result(ResultStatus status, T data, string? msg = null) { return new ResponseResult { Status = status, Data = data, Message = msg }; } }
這里進一步封裝了幾個方法,至于具體封裝幾個這種方法,還是那句話夠用就好,這里我封裝了幾個常用的操作,成功狀態、失敗狀態、異常狀態、最完全狀態,這幾種狀態基本上可以滿足大多數的場景,不夠的話可以自行進行進一步的多封裝幾個方法。這樣的話在action使用的時候就會簡化很多,省去了手動屬性賦值
[HttpGet("GetWeatherForecast")] public ResponseResult> GetAll() { var datas = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }); return ResponseResult >.SuccessResult(datas); }
進一步完善
上面我們通過完善ResponseResult
類的封裝,確實在某些程度上節省了一部分操作,但是還是有點美中不足,那就是每次返回結果的時候,雖然定義了幾個常用的靜態方法去操作返回結果,但是每次還得通過手動去把ResponseResult
類給請出來才能使用,現在呢想在操作返回結果的時候不想看到它了。這個呢也很簡單,我們可以借助微軟針對MVC的Controller的封裝進一步得到一個思路,那就是定義一個基類的Controller,我們在Controller基類中,把常用的返回結果封裝一些方法,這樣在Controller的子類中呢就可以直接調用這些方法,而不需要關注如何去編寫方法的返回類型了,話不多說動手把Controller基類封裝起來
[ApiController] [Route("api/[controller]")] public class ApiControllerBase : ControllerBase { ////// 成功狀態返回結果 /// /// 返回的數據 ///protected ResponseResult SuccessResult (T result) { return ResponseResult .SuccessResult(result); } /// /// 失敗狀態返回結果 /// /// 狀態碼 /// 失敗信息 ///protected ResponseResult FailResult (string? msg = null) { return ResponseResult .FailResult(msg); } /// /// 異常狀態返回結果 /// /// 狀態碼 /// 異常信息 ///protected ResponseResult ErrorResult (string? msg = null) { return ResponseResult .ErrorResult(msg); } /// /// 自定義狀態返回結果 /// /// /// ///protected ResponseResult Result (ResultStatus status, T result, string? msg = null) { return ResponseResult .Result(status, result, msg); } }
有了這么一個大神的輔助,一切似乎又向著美好進發了一步,這樣的話每次我們自定義的Controller可以繼承ApiControllerBase
類,從而使用里面的簡化操作。所以再寫起來代碼,大概是這么一個效果
public class WeatherForecastController : ApiControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; [HttpGet("GetWeatherForecast")] public ResponseResult> GetAll() { var datas = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }); return SuccessResult(datas); } }
這個時候確實變得很美好了,但是還是沒有逃脫一點,那就是我還是得通過特定的方法來得到一個ResponseResult
類型的返回結果,包括我們給ResponseResult
類封裝靜態方法,或者甚至是定義ApiControllerBase
基類,都是為了進一步簡化這個操作。現在呢我想告別這個限制,我能不能把返回的結果直接就默認的轉化成ResponseResult
類型的結果呢?當然可以,這也是通過ASP.NET Core的封裝思路中得到的啟發,借助implicit
自動完成隱式轉換,這個在ASP.NET Core的ActionResult
類中也有體現
public static implicit operator ActionResult(TValue value) { return new ActionResult (value); }
通過這個思路我們可以進一步完善ResponseResult
類的實現方式,給它添加一個隱式轉換的操作,僅僅定義一個方法即可,在ResponseResult
類中繼續完善
////// 隱式將T轉化為ResponseResult /// public static implicit operator ResponseResult/// (T value) { return new ResponseResult { Data = value }; }
這種對于絕大部分返回成功結果的時候提供了非常簡化的操作,這個時候如果你再去使用action的時候就可以進一步來簡化返回值的操作了
[HttpGet("GetWeatherForecast")] public ResponseResult> GetAll() { var datas = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }); return datas.ToList(); }
因為我們定義了T
到ResponseResult
的隱式轉換,所以這個時候我們就可以直接返回結果了,而不需要手動對結果返回值進行包裝。
漏網之魚處理
在上面我們為了盡量簡化action返回ResponseResult
的統一返回結構的封裝,已經對ResponseResult
類進行了許多的封裝,并且還通過封裝ApiControllerBase
基類進一步簡化這一操作,但是終究還是避免不了一點,那就是很多時候可能想不起來對action的返回值去加ResponseResult
類型的返回值,但是我們之前的所有封裝都得建立在必須要聲明ResponseResult
類型的返回值的基礎上才行,否則就不存在統一返回格式這一說法了。所以針對這些漏網之魚,我們必須要有統一的攔截機制,這樣才能更完整的針對返回結果進行處理,針對這種對action返回值的操作,我們首先想到的就是定義過濾器
進行處理,因此筆者針對這一現象封裝了一個統一包裝結果的過濾器,實現如下
public class ResultWrapperFilter : ActionFilterAttribute { public override void OnResultExecuting(ResultExecutingContext context) { var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; var actionWrapper = controllerActionDescriptor?.MethodInfo.GetCustomAttributes(typeof(NoWrapperAttribute), false).FirstOrDefault(); var controllerWrapper = controllerActionDescriptor?.ControllerTypeInfo.GetCustomAttributes(typeof(NoWrapperAttribute), false).FirstOrDefault(); //如果包含NoWrapperAttribute則說明不需要對返回結果進行包裝,直接返回原始值 if (actionWrapper != null || controllerWrapper != null) { return; } //根據實際需求進行具體實現 var rspResult = new ResponseResult
在使用WebAPI的過程中,我們的action絕大部分是直接返回ViewModel
或Dto
而并沒有返回ActionResult
類型相關,但是無妨,這個時候MVC的底層操作會為我們將這些自定義的類型包裝成ObjectResult
類型的,因此我們的ResultWrapperFilter
過濾器也是通過這一機制進行操作的。這里有兩點需要考慮的
- 首先是,我們必須要允許并非所有的返回結果都要進行
ResponseResult
的包裝,為了滿足這一需求我們還定義了NoWrapperAttribute
來實現這一效果,只要Controller或Action有NoWrapperAttribute
的修飾則不對返回結果進行任何處理。 - 其次是,如果我們的Action上的返回類型已經是
ResponseResult
類型的,則也不需要對返回結果進行再次的包裝。
關于ResultWrapperFilter
的定義其實很簡單,因為在這里它只是起到了一個標記的作用
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] public class NoWrapperAttribute:Attribute { }
到了這里,還有一種特殊的情況需要注意,那就是當程序發生異常的時候,我們上面的這些機制也是沒有辦法生效的,因此我們還需要定義一個針對全局異常處理的攔截機制,同樣是可以使用統一異常處理過濾器進行操作,實現如下
public class GlobalExceptionFilter : IExceptionFilter { private readonly ILogger_logger; public GlobalExceptionFilter(ILogger logger) { _logger = logger; } public void OnException(ExceptionContext context) { //異常返回結果包裝 var rspResult = ResponseResult
寫完過濾器了,千萬不能忘了全局注冊一下,否則它也就只能看看了,不會起到任何效果
builder.Services.AddControllers(options => { options.Filters.Add(); options.Filters.Add (); });
漏網之魚另一種處理
當然針對上面兩種針對漏網之魚的處理,在ASP.NET Core上還可以通過中間件的方式進行處理,至于過濾器和中間件有何不同,相信大家已經非常清楚了,核心不同總結起來就一句話二者的處理階段不同,即針對管道的生命周期處理是不一樣的,中間件可以處理任何生命周期在它之后的場景,但是過濾器只管理Controller這一塊的一畝三分地
但是針對結果包裝這一場景,筆者覺得使用過濾器的方式更容易處理一點,因為畢竟我們是要操作Action的返回結果,通過過濾器中我們可以直接拿到返回結果的值。但是這個操作如果在中間件里進行操作的話,只能通過讀取Response.Body
進行操作了,筆者這里也封裝了一個操作,如下所示
public static IApplicationBuilder UseResultWrapper(this IApplicationBuilder app) { var serializerOptions = app.ApplicationServices.GetRequiredService>().Value.JsonSerializerOptions; serializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; return app.Use(async (context, next) => { var originalResponseBody = context.Response.Body; try { //因為Response.Body沒辦法進行直接讀取,所以需要特殊操作一下 using var swapStream = new MemoryStream(); context.Response.Body = swapStream; await next(); //判斷是否出現了異常狀態碼,需要特殊處理 if (context.Response.StatusCode == StatusCodes.Status500InternalServerError) { context.Response.Body.Seek(0, SeekOrigin.Begin); await swapStream.CopyToAsync(originalResponseBody); return; } var endpoint = context.Features.Get ()?.Endpoint; if (endpoint != null) { //只針對application/json結果進行處理 if (context.Response.ContentType.ToLower().Contains("application/json")) { //判斷終結點是否包含NoWrapperAttribute NoWrapperAttribute noWrapper = endpoint.Metadata.GetMetadata (); if (noWrapper != null) { context.Response.Body.Seek(0, SeekOrigin.Begin); await swapStream.CopyToAsync(originalResponseBody); return; } //獲取Action的返回類型 var controllerActionDescriptor = context.GetEndpoint()?.Metadata.GetMetadata (); if (controllerActionDescriptor != null) { //泛型的特殊處理 var returnType = controllerActionDescriptor.MethodInfo.ReturnType; if (returnType.IsGenericType && (returnType.GetGenericTypeDefinition() == typeof(Task<>) || returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))) { returnType = returnType.GetGenericArguments()[0]; } //如果終結點已經是ResponseResult 則不進行包裝處理 if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ResponseResult<>)) { context.Response.Body.Seek(0, SeekOrigin.Begin); await swapStream.CopyToAsync(originalResponseBody); return; } context.Response.Body.Seek(0, SeekOrigin.Begin); //反序列化得到原始結果 var result = await JsonSerializer.DeserializeAsync(context.Response.Body, returnType, serializerOptions); //對原始結果進行包裝 var bytes = JsonSerializer.SerializeToUtf8Bytes(ResponseResult
相信通過上面的處理,我們就可以更容易的看出來,誰更容易的對統一結果進行包裝處理了,畢竟我們是針對Action的返回結果進行處理,而過濾器顯然就是為針對Controller和Action的處理而生的。但是通過中間件的方式能更完整的針對結果進行處理,因為許多時候我們可能是在自定義的中間件里直接攔截請求并返回,但是根據二八原則這種情況相對于Action的返回值畢竟是少數,有這種情況我們可以通過直接ResponseResult
封裝的方法進行返回操作,也很方便。但是這個時候呢,關于異常處理我們通過全局異常處理中間件,則能更多的處理更多的場景,且沒有副作用,看一下它的定義
public static IApplicationBuilder UseException(this IApplicationBuilder app) { return app.UseExceptionHandler(configure => { configure.Run(async context => { var exceptionHandlerPathFeature = context.Features.Get(); var ex = exceptionHandlerPathFeature?.Error; if (ex != null) { var _logger = context.RequestServices.GetService >(); var rspResult = ResponseResult
使用全局異常梳理中間件是沒有副作用的,主要因為在異常處理的時候我們不需要讀取Response.Body
進行讀取操作的,所以至于是選擇異常處理中間件還是過濾器,大家可以針對自己的實際場景進行選擇,兩種方式都是可以的。
總結
本文主要是展示了針對ASP.NET Core WeApi結果統一返回格式的相關操作,通過示例我們一步一步的展示了完成這一目標的不斷升級的實現,雖然整體看起來比較簡單,但是卻承載著筆者一次又一次的思考升級。每次實現完一個階段,都會去想有沒有更好的方式去完善它。這其中還有一些思路來自微軟源碼為我們提供的思路,所以很多時候還是建議大家去看一看源碼的,可以在很多時候為我們提供一種解決問題的思路。正如我看到的一句話,讀源碼也是一種圍城,外面的人不想進去,里面的人不想出來。如果大家有更好的實現方式,歡迎一起討論。曾經的時候我會為自己學到了一個新的技能而感到高興,到了后來我會對有一個好的思路,或者好的解決問題的方法而感到高興。讀萬卷書很重要,行萬里路同樣重要,讀書是沉淀,行路是實踐,結合到一起才能更好的促進,而不是只選擇一種。
原文鏈接:https://www.cnblogs.com/wucy/p/16124449.html
相關推薦
- 2022-03-14 Token跨域問題Response to preflight request doesn‘t pas
- 2022-12-07 C++?基本數據類型中int、long等整數類型取值范圍及原理分析_C 語言
- 2022-01-30 tortoisegit pull時報錯
- 2022-09-13 Android四大組件之Service服務詳細講解_Android
- 2023-03-28 python中向二維數組中添加整行或者增列元素問題_python
- 2023-07-30 element中對el-input 自定義驗證規則
- 2022-12-06 C++中調用復制(拷貝)函數的三種情況總結_C 語言
- 2022-08-11 GoFrame框架數據校驗之校驗結果Error接口對象_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支