您现在的位置是:网站首页> 编程资料编程资料

ASP.NET Core WebApi返回结果统一包装实践记录_实用技巧_

2023-05-24 523人已围观

简介 ASP.NET Core WebApi返回结果统一包装实践记录_实用技巧_

前言

近期在重新搭建一套基于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(); } 

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

漏网之鱼处理

在上面我们为了尽量简化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) 
                
                

-六神源码网