asp.net core webapi返回結果統一包裝實踐

asp.net core webapi返回結果統一包裝實踐

關於webapi統一結果返回的時候,讓我也有了更一步的思考,首先是如何能更好的限制返回統一的格式,其次是關於結果的包裝一定是更簡單更強大。在不斷的思考和完善中,終於有了初步的成果,便分享出來,學無止境思考便無止境,希望以此能與君共勉。

最后更新 2022/4/13 上午7:12
yi念之间
预计阅读 18 分钟
分类
ASP.NET Core
标签
.NET C# ASP.NET Core Web API

前言

近期在重新搭建一套基於 asp.net core webapi 的框架,這其中確實帶來了不少的收穫,畢竟當你想搭建一套框架的時候,你總會不自覺的去想,如何讓這套框架變得更完善一點更好用一點。其中在關於 webapi 統一結果返回的時候,讓我也有了更一步的思考,首先是如何能更好的限制返回統一的格式,其次是關於結果的包裝一定是更簡單更強大。在不斷的思考和完善中,終於有了初步的成果,便分享出來,學無止境思考便無止境,希望以此能與君共勉。

統一結果類封裝

首先如果讓返回的結果格式統一,就得有一個統一的包裝類去包裝所有的返回結果,因為返回的具體數據雖然格式一致,但是具體的值的類型是不確定的,因此我們這裡需要定義個泛型類。當然如果你不選擇泛型類的話用 dynamic 或者 object 類型也是可以的,但是這樣的話可能會帶來兩點不足

  • 一是可能會存在裝箱拆箱的操作。
  • 二是如果引入 swagger 的話是沒辦法生成返回的類型的,因為 dynamic 或 object 類型都是執行具體的 action 時才能確定返回類型的,但是 swagger 的結構是首次運行的時候就獲取到的,因此無法感知具體類型。

定義包裝類

上面我們也說了關於定義泛型類的優勢,這裡就話不多說來直接封裝一個結果返回的包裝類

public class ResponseResult<T>
{
    /// <summary>
    /// 状态结果
    /// </summary>
    public ResultStatus Status { get; set; } = ResultStatus.Success;

    private string? _msg;

    /// <summary>
    /// 消息描述
    /// </summary>
    public string? Message
    {
        get
        {
            // 如果没有自定义的结果描述,则可以获取当前状态的描述
            return !string.IsNullOrEmpty(_msg) ? _msg : EnumHelper.GetDescription(Status);
        }
        set
        {
            _msg = value;
        }
    }

    /// <summary>
    /// 返回结果
    /// </summary>
    public T Data { get; set; }
}

其中这里的ResultStatus是一个枚举类型,用于定义具体的返回状态码,用于判断返回的结果是正常还是异常或者其他,我这里只是简单的定义了一个最简单的示例,有需要的话也可以自行扩展

public enum ResultStatus
{
    [Description("请求成功")]
    Success = 1,
    [Description("请求失败")]
    Fail = 0,
    [Description("请求异常")]
    Error = -1
}

这种情况下定义枚举类型并且结合它的DescriptionAttribute的特性去描述枚举的含义是一个不错的选择,首先它可以统一管理每个状态的含义,其次是更方便的获取每个状态对应的描述。这样的话如果没有自定义的结果描述,则可以获取当前状态的描述来充当默认值的情况。这个时候在写具体 action 的时候会是以下的效果

[HttpGet("GetWeatherForecast")]
public ResponseResult<IEnumerable<WeatherForecast>> 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<IEnumerable<WeatherForecast>> {  Data = datas };
}

这样的话每次编写 action 的时候都可以返回一个ResponseResult<T>的结果了,这里就体现出了使用枚举定义状态码的优势了,相当一部分场景我们可以省略了状态码甚至是消息的编写,毕竟很多时候在保障功能的情况下,代码还是越简介越好的,更何况是一些高频操作呢。

升級一下操作

上面虽然我们定义了ResponseResult<T>来统一包装返回结果,但是每次还得 new 一下,在无疑是不太方便的,而且还要每次都还得给属性赋值啥的,也是够麻烦的,这个时候就想,如果能有相关的辅助方法去简化操作就好了,方法不用太多能满足场景就好,也就是够用就好,最主要的是能支持扩展就可以。因此,进一步升级一下结果包装类,来简化一下操作

public class ResponseResult<T>
{
    /// <summary>
    /// 状态结果
    /// </summary>
    public ResultStatus Status { get; set; } = ResultStatus.Success;

    private string? _msg;

    /// <summary>
    /// 消息描述
    /// </summary>
    public string? Message
    {
        get
        {
            return !string.IsNullOrEmpty(_msg) ? _msg : EnumHelper.GetDescription(Status);
        }
        set
        {
            _msg = value;
        }
    }

    /// <summary>
    /// 返回结果
    /// </summary>
    public T Data { get; set; }

    /// <summary>
    /// 成功状态返回结果
    /// </summary>
    /// <param name="result">返回的数据</param>
    /// <returns></returns>
    public static ResponseResult<T> SuccessResult(T data)
    {
        return new ResponseResult<T> { Status = ResultStatus.Success, Data = data };
    }

    /// <summary>
    /// 失败状态返回结果
    /// </summary>
    /// <param name="code">状态码</param>
    /// <param name="msg">失败信息</param>
    /// <returns></returns>
    public static ResponseResult<T> FailResult(string? msg = null)
    {
        return new ResponseResult<T> { Status = ResultStatus.Fail, Message = msg };
    }

    /// <summary>
    /// 异常状态返回结果
    /// </summary>
    /// <param name="code">状态码</param>
    /// <param name="msg">异常信息</param>
    /// <returns></returns>
    public static ResponseResult<T> ErrorResult(string? msg = null)
    {
        return new ResponseResult<T> { Status = ResultStatus.Error, Message = msg };
    }

    /// <summary>
    /// 自定义状态返回结果
    /// </summary>
    /// <param name="status"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    public static ResponseResult<T> Result(ResultStatus status, T data, string? msg = null)
    {
        return new ResponseResult<T> { Status = status, Data = data, Message = msg };
    }
}

這裡進一步封裝了幾個方法,至於具體封裝幾個這種方法,還是那句話夠用就好,這裡我封裝了幾個常用的操作,成功狀態、失敗狀態、異常狀態、最完全狀態,這幾種狀態基本上可以滿足大多數的場景,不夠的話可以自行進行進一步的多封裝幾個方法。這樣的話在 action 使用的時候就會簡化很多,省去了手動屬性賦值

[HttpGet("GetWeatherForecast")]
public ResponseResult<IEnumerable<WeatherForecast>> 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<IEnumerable<WeatherForecast>>.SuccessResult(datas);
}

進一步完善

上面我们通过完善ResponseResult<T>类的封装,确实在某些程度上节省了一部分操作,但是还是有点美中不足,那就是每次返回结果的时候,虽然定义了几个常用的静态方法去操作返回结果,但是每次还得通过手动去把ResponseResult<T>类给请出来才能使用,现在呢想在操作返回结果的时候不想看到它了。这个呢也很简单,我们可以借助微软针对 MVC 的 Controller 的封装进一步得到一个思路,那就是定义一个基类的 Controller,我们在 Controller 基类中,把常用的返回结果封装一些方法,这样在 Controller 的子类中呢就可以直接调用这些方法,而不需要关注如何去编写方法的返回类型了,话不多说动手把 Controller 基类封装起来

[ApiController]
[Route("api/[controller]")]
public class ApiControllerBase : ControllerBase
{
    /// <summary>
    /// 成功状态返回结果
    /// </summary>
    /// <param name="result">返回的数据</param>
    /// <returns></returns>
    protected ResponseResult<T> SuccessResult<T>(T result)
    {
        return ResponseResult<T>.SuccessResult(result);
    }

    /// <summary>
    /// 失败状态返回结果
    /// </summary>
    /// <param name="code">状态码</param>
    /// <param name="msg">失败信息</param>
    /// <returns></returns>
    protected ResponseResult<T> FailResult<T>(string? msg = null)
    {
        return ResponseResult<T>.FailResult(msg);
    }

    /// <summary>
    /// 异常状态返回结果
    /// </summary>
    /// <param name="code">状态码</param>
    /// <param name="msg">异常信息</param>
    /// <returns></returns>
    protected ResponseResult<T> ErrorResult<T>(string? msg = null)
    {
        return ResponseResult<T>.ErrorResult(msg);
    }

    /// <summary>
    /// 自定义状态返回结果
    /// </summary>
    /// <param name="status"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    protected ResponseResult<T> Result<T>(ResultStatus status, T result, string? msg = null)
    {
        return ResponseResult<T>.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<IEnumerable<WeatherForecast>> 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<T>类型的返回结果,包括我们给ResponseResult<T>类封装静态方法,或者甚至是定义ApiControllerBase基类,都是为了进一步简化这个操作。现在呢我想告别这个限制,我能不能把返回的结果直接就默认的转化成ResponseResult<T>类型的结果呢?当然可以,这也是通过 ASP.NET Core 的封装思路中得到的启发,借助implicit自动完成隐式转换,这个在 ASP.NET Core 的ActionResult<T>类中也有体现

public static implicit operator ActionResult<TValue>(TValue value)
{
    return new ActionResult<TValue>(value);
}

通过这个思路我们可以进一步完善ResponseResult<T>类的实现方式,给它添加一个隐式转换的操作,仅仅定义一个方法即可,在ResponseResult<T>类中继续完善

/// <summary>
/// 隐式将T转化为ResponseResult<T>
/// </summary>
/// <param name="value"></param>
public static implicit operator ResponseResult<T>(T value)
{
    return new ResponseResult<T> { Data = value };
}

這種對於絕大部分返回成功結果的時候提供了非常簡化的操作,這個時候如果你再去使用 action 的時候就可以進一步來簡化返回值的操作了

[HttpGet("GetWeatherForecast")]
public ResponseResult<IEnumerable<WeatherForecast>> 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();
}

因为我们定义了TResponseResult<T>的隐式转换,所以这个时候我们就可以直接返回结果了,而不需要手动对结果返回值进行包装。

漏網之魚處理

在上面我们为了尽量简化 action 返回ResponseResult<T>的统一返回结构的封装,已经对ResponseResult<T>类进行了许多的封装,并且还通过封装ApiControllerBase基类进一步简化这一操作,但是终究还是避免不了一点,那就是很多时候可能想不起来对 action 的返回值去加ResponseResult<T>类型的返回值,但是我们之前的所有封装都得建立在必须要声明ResponseResult<T>类型的返回值的基础上才行,否则就不存在统一返回格式这一说法了。所以针对这些漏网之鱼,我们必须要有统一的拦截机制,这样才能更完整的针对返回结果进行处理,针对这种对 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<object>();
        if (context.Result is ObjectResult)
        {
            var objectResult = context.Result as ObjectResult;
            if (objectResult?.Value == null)
            {
                rspResult.Status = ResultStatus.Fail;
                rspResult.Message = "未找到资源";
                context.Result = new ObjectResult(rspResult);
            }
            else
            {
                //如果返回结果已经是ResponseResult<T>类型的则不需要进行再次包装了
                if (objectResult.DeclaredType.IsGenericType && objectResult.DeclaredType?.GetGenericTypeDefinition() == typeof(ResponseResult<>))
                {
                    return;
                }
                rspResult.Data = objectResult.Value;
                context.Result = new ObjectResult(rspResult);
            }
            return;
        }
    }
}

在使用 WebAPI 的过程中,我们的 action 绝大部分是直接返回ViewModelDto而并没有返回ActionResult类型相关,但是无妨,这个时候 MVC 的底层操作会为我们将这些自定义的类型包装成ObjectResult类型的,因此我们的ResultWrapperFilter过滤器也是通过这一机制进行操作的。这里有两点需要考虑的

  • 首先是,我们必须要允许并非所有的返回结果都要进行ResponseResult<T>的包装,为了满足这一需求我们还定义了NoWrapperAttribute来实现这一效果,只要 Controller 或 Action 有NoWrapperAttribute的修饰则不对返回结果进行任何处理。
  • 其次是,如果我们的 Action 上的返回类型已经是ResponseResult<T>类型的,则也不需要对返回结果进行再次的包装。

关于ResultWrapperFilter的定义其实很简单,因为在这里它只是起到了一个标记的作用

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class NoWrapperAttribute:Attribute
{
}

到了這裡,還有一種特殊的情況需要注意,那就是當程式發生異常的時候,我們上面的這些機制也是沒有辦法生效的,因此我們還需要定義一個針對全局異常處理的攔截機制,同樣是可以使用統一異常處理過濾器進行操作,實現如下

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;
    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        //异常返回结果包装
        var rspResult = ResponseResult<object>.ErrorResult(context.Exception.Message);
        //日志记录
        _logger.LogError(context.Exception, context.Exception.Message);
        context.ExceptionHandled = true;
        context.Result = new InternalServerErrorObjectResult(rspResult);
    }

    public class InternalServerErrorObjectResult : ObjectResult
    {
        public InternalServerErrorObjectResult(object value) : base(value)
        {
            StatusCode = StatusCodes.Status500InternalServerError;
        }
    }
}

寫完過濾器了,千萬不能忘了全局註冊一下,否則它也就只能看看了,不會起到任何效果

builder.Services.AddControllers(options =>
{
    options.Filters.Add<ResultWrapperFilter>();
    options.Filters.Add<GlobalExceptionFilter>();
});

漏網之魚另一種處理

當然針對上面兩種針對漏網之魚的處理,在 asp.net core 上還可以通過中間件的方式進行處理,至於過濾器和中間件有何不同,相信大家已經非常清楚了,核心不同總結起來就一句話

二者的處理階段不同,即針對管道的生命周期處理是不一樣的,中間件可以處理任何生命周期在它之後的場景,但是過濾器只管理 controller 這一塊的一畝三分地

但是针对结果包装这一场景,笔者觉得使用过滤器的方式更容易处理一点,因为毕竟我们是要操作 Action 的返回结果,通过过滤器中我们可以直接拿到返回结果的值。但是这个操作如果在中间件里进行操作的话,只能通过读取Response.Body进行操作了,笔者这里也封装了一个操作,如下所示

public static IApplicationBuilder UseResultWrapper(this IApplicationBuilder app)
{
        var serializerOptions = app.ApplicationServices.GetRequiredService<IOptions<JsonOptions>>().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<IEndpointFeature>()?.Endpoint;
                if (endpoint != null)
                {
                    //只针对application/json结果进行处理
                    if (context.Response.ContentType.ToLower().Contains("application/json"))
                    {
                        //判断终结点是否包含NoWrapperAttribute
                        NoWrapperAttribute noWrapper = endpoint.Metadata.GetMetadata<NoWrapperAttribute>();
                        if (noWrapper != null)
                        {
                            context.Response.Body.Seek(0, SeekOrigin.Begin);
                            await swapStream.CopyToAsync(originalResponseBody);
                            return;
                        }
                        //获取Action的返回类型
                        var controllerActionDescriptor = context.GetEndpoint()?.Metadata.GetMetadata<ControllerActionDescriptor>();
                        if (controllerActionDescriptor != null)
                        {
                            //泛型的特殊处理
                            var returnType = controllerActionDescriptor.MethodInfo.ReturnType;
                            if (returnType.IsGenericType && (returnType.GetGenericTypeDefinition() == typeof(Task<>) || returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))
                            {
                                returnType = returnType.GetGenericArguments()[0];
                            }
                            //如果终结点已经是ResponseResult<T>则不进行包装处理
                            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<object>.SuccessResult(result), serializerOptions);
                            new MemoryStream(bytes).CopyTo(originalResponseBody);
                            return;
                        }
                    }
                }
                context.Response.Body.Seek(0, SeekOrigin.Begin);
                await swapStream.CopyToAsync(originalResponseBody);
            }
            finally
            {
                //将原始的Body归还回来
                context.Response.Body = originalResponseBody;
            }
        });
    }
}

相信通过上面的处理,我们就可以更容易的看出来,谁更容易的对统一结果进行包装处理了,毕竟我们是针对 Action 的返回结果进行处理,而过滤器显然就是为针对 Controller 和 Action 的处理而生的。但是通过中间件的方式能更完整的针对结果进行处理,因为许多时候我们可能是在自定义的中间件里直接拦截请求并返回,但是根据二八原则这种情况相对于 Action 的返回值毕竟是少数,有这种情况我们可以通过直接ResponseResult<T>封装的方法进行返回操作,也很方便。但是这个时候呢,关于异常处理我们通过全局异常处理中间件,则能更多的处理更多的场景,且没有副作用,看一下它的定义

public static IApplicationBuilder UseException(this IApplicationBuilder app)
{
    return app.UseExceptionHandler(configure =>
    {
        configure.Run(async context =>
        {
            var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
            var ex = exceptionHandlerPathFeature?.Error;
            if (ex != null)
            {
                var _logger = context.RequestServices.GetService<ILogger<IExceptionHandlerPathFeature>>();
                var rspResult = ResponseResult<object>.ErrorResult(ex.Message);
                _logger?.LogError(ex, message: ex.Message);
                context.Response.StatusCode = StatusCodes.Status500InternalServerError;
                context.Response.ContentType = "application/json;charset=utf-8";
                await context.Response.WriteAsync(rspResult.SerializeObject());
            }
        });
    });
}

使用全局異常梳理中間件是沒有副作用的,主要因為在異常處理的時候我們不需要讀取 response.body 進行讀取操作的,所以至於是選擇異常處理中間件還是過濾器,大家可以針對自己的實際場景進行選擇,兩種方式都是可以的。

總結

本文主要是展示了針對 asp.net core weapi 結果統一返回格式的相關操作,通過示例我們一步一步的展示了完成這一目標的不斷升級的實現,雖然整體看起來比較簡單,但是卻承載著筆者一次又一次的思考升級。每次實現完一個階段,都會去想有沒有更好的方式去完善它。這其中還有一些思路來自微軟源碼為我們提供的思路,所以很多時候還是建議大家去看一看源碼的,可以在很多時候為我們提供一種解決問題的思路。正如我看到的一句話,讀源碼也是一種圍城,外面的人不想進去,裡面的人不想出來。如果大家有更好的實現方式,歡迎一起討論。曾經的時候我會為自己學到了一個新的技能而感到高興,到了後來我會對有一個好的思路,或者好的解決問題的方法而感到高興。讀萬卷書很重要,行萬里路同樣重要,讀書是沉澱,行路是實踐,結合到一起才能更好的促進,而不是只選擇一種。

👇 歡迎掃碼關注我的公眾號👇

Keep Exploring

延伸阅读

更多文章