<xmp id="63nn9"><video id="63nn9"></video></xmp>

<xmp id="63nn9"></xmp>

<wbr id="63nn9"><ins id="63nn9"></ins></wbr>

<wbr id="63nn9"></wbr><video id="63nn9"><ins id="63nn9"><table id="63nn9"></table></ins></video>

Loading

基于.NetCore開發博客項目 StarBlog - (24) 統一接口數據返回格式

前言

開發接口,是給客戶端(Web前端、App)用的,前面說的RESTFul,是接口的規范,有了統一的接口風格,客戶端開發人員在訪問后端功能的時候能更快找到需要的接口,能寫出可維護性更高的代碼。

而接口的數據返回格式也是接口規范的重要一環,不然一個接口返回JSON,一個返回純字符串,客戶端對接到數據時一臉懵逼,沒法處理啊。

合格的接口返回值應該包括狀態碼、提示信息和數據。

就像這樣:

{
  "statusCode": 200,
  "successful": true,
  "message": null,
  "data": {}
}

默認AspNetCoreWebAPI模板是沒有特定的返回格式,因為這些業務性質的東西需要開發者自己來定義和完成。

在前面的文章中,可以看到本項目的接口返回值都是 ApiResponse 及其派生類型,這就是在StarBlog里定制的統一返回格式。事實上我的其他項目也在用這套接口返回值,這已經算是一個 Utilities 性質的組件了。

PS:今天寫這篇文章時,我順手把這個返回值發布了一個nuget包,以后在其他項目里使用就不用復制粘貼了~

分析一下

在 AspNetCore 里寫 WebApi ,我們的 Controller 需要繼承 ControllerBase 這個類

接口 Action 可以設置返回值為 IActionResultActionResult<T> 類型,然后返回數據的時候,可以使用 ControllerBase 封裝好的 Ok(), NotFound() 等方法,這些方法在返回數據的同時會自動設置響應的HTTP狀態碼。

PS:關于 IActionResultActionResult<T> 這倆的區別請參考官方文檔。

本文只提關鍵的一點:ActionResult<T>返回類型可以讓接口在swagger文檔中直觀看出返回的數據類型。

所以我們不僅要封裝統一的返回值,還要實現類似 Ok(), NotFound(), BadRequest() 的快捷方法。

顯然當接口返回類型全都是 ApiResponse<T> 時,這樣返回的狀態碼都是200,不符合需求。

而且有些接口之前已經寫好了,返回類型是 List<T> 這類的,我們也要把這些接口的返回值包裝起來,統一返回格式。

要解決這些問題,我們得了解一下 AspNetCore 的管道模型。

AspNetCore 管道模型

最外層,是中間件,一個請求進來,經過一個個中間件,到最后一個中間件,生成響應,再依次經過一個個中間件走出來,得到最終響應。

image

常用的 AspNetCore 項目中間件有這些,如下圖所示:

image

最后的 Endpoint 就是最終生成響應的中間件。

在本項目中,Program.cs 配置里的最后一個中間件,就是添加了一個處理 MVC 的 Endpoint

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

這個 Endpoint 的結構又是這樣的:

image

可以看到有很多 Filter 包圍在用戶代碼的前后。

所以得出結論,要修改請求的響應,我們可以選擇:

  • 寫一個中間件處理
  • 使用過濾器(Filter)

那么,來開始寫代碼吧~

定義ApiResponse

首先是這個出現頻率很高的 ApiResponse,終于要揭曉了~

StarBlog.Web/ViewModels/Response 命名空間下,我創建了三個文件,分別是:

  • ApiResponse.cs
  • ApiResponsePaged.cs: 分頁響應
  • IApiResponse.cs: 幾個相關的接口

ApiResponse.cs 中,其實是兩個類,一個 ApiResponse<T> ,另一個 ApiResponse,帶泛型和不帶泛型。

PS:C#的泛型有點復雜,當時搞這東西搞得暈暈的,又復習了一些逆變和協變,不過最終沒有用上。

接口代碼

上代碼,先是幾個接口的代碼

public interface IApiResponse {
    public int StatusCode { get; set; }
    public bool Successful { get; set; }
    public string? Message { get; set; }
}

public interface IApiResponse<T> : IApiResponse {
    public T? Data { get; set; }
}

public interface IApiErrorResponse {
    public Dictionary<string,object> ErrorData { get; set; }
}

保證了所有相關對象都來自 IApiResponse 接口。

ApiResponse<T>

接著看 ApiResponse<T> 的代碼。

public class ApiResponse<T> : IApiResponse<T> {
    public ApiResponse() {
    }

    public ApiResponse(T? data) {
        Data = data;
    }

    public int StatusCode { get; set; } = 200;
    public bool Successful { get; set; } = true;
    public string? Message { get; set; }

    public T? Data { get; set; }

    /// <summary>
    /// 實現將 <see cref="ApiResponse"/> 隱式轉換為 <see cref="ApiResponse{T}"/>
    /// </summary>
    /// <param name="apiResponse"><see cref="ApiResponse"/></param>
    public static implicit operator ApiResponse<T>(ApiResponse apiResponse) {
        return new ApiResponse<T> {
            StatusCode = apiResponse.StatusCode,
            Successful = apiResponse.Successful,
            Message = apiResponse.Message
        };
    }
}

這里使用運算符重載,實現了 ApiResponseApiResponse<T> 的隱式轉換。

等下就能看出有啥用了~

ApiResponse

繼續看 ApiResponse 代碼,比較長,封裝了幾個常用的方法在里面,會有一些重復代碼。

這個類實現了倆接口:IApiResponse, IApiErrorResponse

public class ApiResponse : IApiResponse, IApiErrorResponse {
    public int StatusCode { get; set; } = 200;
    public bool Successful { get; set; } = true;
    public string? Message { get; set; }
    public object? Data { get; set; }

    /// <summary>
    /// 可序列化的錯誤
    /// <para>用于保存模型驗證失敗的錯誤信息</para>
    /// </summary>
    public Dictionary<string,object>? ErrorData { get; set; }

    public ApiResponse() {
    }

    public ApiResponse(object data) {
        Data = data;
    }

    public static ApiResponse NoContent(string message = "NoContent") {
        return new ApiResponse {
            StatusCode = StatusCodes.Status204NoContent,
            Successful = true, Message = message
        };
    }

    public static ApiResponse Ok(string message = "Ok") {
        return new ApiResponse {
            StatusCode = StatusCodes.Status200OK,
            Successful = true, Message = message
        };
    }

    public static ApiResponse Ok(object data, string message = "Ok") {
        return new ApiResponse {
            StatusCode = StatusCodes.Status200OK,
            Successful = true, Message = message,
            Data = data
        };
    }

    public static ApiResponse Unauthorized(string message = "Unauthorized") {
        return new ApiResponse {
            StatusCode = StatusCodes.Status401Unauthorized,
            Successful = false, Message = message
        };
    }

    public static ApiResponse NotFound(string message = "NotFound") {
        return new ApiResponse {
            StatusCode = StatusCodes.Status404NotFound,
            Successful = false, Message = message
        };
    }

    public static ApiResponse BadRequest(string message = "BadRequest") {
        return new ApiResponse {
            StatusCode = StatusCodes.Status400BadRequest,
            Successful = false, Message = message
        };
    }

    public static ApiResponse BadRequest(ModelStateDictionary modelState, string message = "ModelState is not valid.") {
        return new ApiResponse {
            StatusCode = StatusCodes.Status400BadRequest,
            Successful = false, Message = message,
            ErrorData = new SerializableError(modelState)
        };
    }

    public static ApiResponse Error(string message = "Error", Exception? exception = null) {
        object? data = null;
        if (exception != null) {
            data = new {
                exception.Message,
                exception.Data
            };
        }

        return new ApiResponse {
            StatusCode = StatusCodes.Status500InternalServerError,
            Successful = false,
            Message = message,
            Data = data
        };
    }
}

ApiResponsePaged<T>

這個分頁是最簡單的,只是多了個 Pagination 屬性而已

public class ApiResponsePaged<T> : ApiResponse<List<T>> where T : class {
    public ApiResponsePaged() {
    }

    public ApiResponsePaged(IPagedList<T> pagedList) {
        Data = pagedList.ToList();
        Pagination = pagedList.ToPaginationMetadata();
    }

    public PaginationMetadata? Pagination { get; set; }
}

類型隱式轉換

來看這個接口

public ApiResponse<Post> Get(string id) {
    var post = _postService.GetById(id);
    return post == null ? ApiResponse.NotFound() : new ApiResponse<Post>(post);
}

根據上面的代碼,可以發現 ApiResponse.NotFound() 返回的是一個 ApiResponse 對象

但這接口的返回值明明是 ApiResponse<Post> 類型呀,這不是類型不一致嗎?

不過在 ApiResponse<T> 中,我們定義了一個運算符重載,實現了 ApiResponse 類型到 ApiResponse<T> 的隱式轉換,所以就完美解決這個問題,大大減少了代碼量。

不然原本是要寫成這樣的

return post == null ? 
    new ApiResponse<Post> {
	    StatusCode = StatusCodes.Status404NotFound,
    	Successful = false, Message = "未找到"
	} : 
	new ApiResponse<Post>(post);

現在只需簡簡單單的 ApiResponse.NotFound(),就跟 AspNetCore 自帶的一樣妙~

包裝返回值

除了這些以 ApiResponseApiResponse<T> 作為返回類型的接口,還有很多其他返回類型的接口,比如

public List<ConfigItem> GetAll() {
    return _service.GetAll();
}

還有

public async Task<string> Poem() {
    return await _crawlService.GetPoem();
}

這些接口在 AspNetCore 生成響應的時候,會把這些返回值歸類為 ObjectResult ,如果不做處理,就會直接序列化成不符合我們返回值規范的格式。

這個不行,必須對這部分接口的返回格式也統一起來。

因為種種原因,最終我選擇使用過濾器來實現這個功能。

關于過濾器的詳細用法,可以參考官方文檔,本文就不展開了,直接上代碼。

創建文件 StarBlog.Web/Filters/ResponseWrapperFilter.cs

public class ResponseWrapperFilter : IAsyncResultFilter {
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) {
        if (context.Result is ObjectResult objectResult) {
            if (objectResult.Value is IApiResponse apiResponse) {
                objectResult.StatusCode = apiResponse.StatusCode;
                context.HttpContext.Response.StatusCode = apiResponse.StatusCode;
            }
            else {
                var statusCode = objectResult.StatusCode ?? context.HttpContext.Response.StatusCode;

                var wrapperResp = new ApiResponse<object> {
                    StatusCode = statusCode,
                    Successful = statusCode is >= 200 and < 400,
                    Data = objectResult.Value,
                };

                objectResult.Value = wrapperResp;
                objectResult.DeclaredType = wrapperResp.GetType();
            }
        }

        await next();
    }
}

在代碼中進行判斷,當響應的類型是 ObjectResult 時,把這個響應結果拿出來,再判斷是不是 IApiResponse 類型。

前面我們介紹過,所有 ApiResponse 都實現了 IApiResponse 這個接口,所以可以判斷是不是 IApiResponse 類型來確定這個返回結果是否包裝過。

沒包裝的話就給包裝一下,就這么簡單。

之后在 Program.cs 里注冊一下這個過濾器。

var mvcBuilder = builder.Services.AddControllersWithViews(
    options => { options.Filters.Add<ResponseWrapperFilter>(); }
);

搞定

這樣就完事兒啦~

最后所有接口(可序列化的),返回格式就都變成了這樣

{
  "statusCode": 200,
  "successful": true,
  "message": null,
  "data": {}
}

強迫癥表示舒服了~

PS:對了,返回文件的那類接口除外。

在其他項目中使用

這個 ApiRepsonse ,我已經發布了nuget包

需要在其他項目使用的話,可以直接安裝 CodeLab.Share 這個包

引入 CodeLab.Share.ViewModels.Response 命名空間就完事了~

不用每次都復制粘貼這幾個類,還得改命名空間。

PS:這個包里不包括過濾器!

參考資料

系列文章

posted @ 2022-12-20 23:48  程序設計實驗室  閱讀(1282)  評論(2編輯  收藏  舉報
人碰人摸人爱免费视频播放

<xmp id="63nn9"><video id="63nn9"></video></xmp>

<xmp id="63nn9"></xmp>

<wbr id="63nn9"><ins id="63nn9"></ins></wbr>

<wbr id="63nn9"></wbr><video id="63nn9"><ins id="63nn9"><table id="63nn9"></table></ins></video>