• dotNET Core WebAPI 统一处理(返回值、参数验证、异常)


    现在 Web 开发比较流行前后端分离,我们的产品也是一样,前端使用Vue,后端使用 dotNet Core WebAPI ,在写 API 的过程中有很多地方需要统一处理:

    • 文档
    • 参数验证
    • 返回值
    • 异常处理

    本文就说说 API 的统一处理这些事。

    环境

    dotNet Core:2.1
    VS For Mac:8.1

    文档

    Swagger 是一个 API 文档生成框架,在非 Core 时代就一直在使用,现在前后端分离的模式下,API 文档更是非常重要,让前端开发人员和后端开发人员能更好的沟通和合作,前端开发人员在 Swagger 可以了解到接口的地址、入参、出参,还能模拟调用,非常方便。

    安装

    在 VS For Mac 中创建 API 项目 DotNetCoreApiSample ,在依赖项中的 NuGet 上点击右键,选择添加包,如下图:

    搜索 Swashbuckle.AspNetCore,选中搜索结果的第一条,点击「添加包」按钮进行添加。

    配置

    Startup 类的 ConfigureServices 方法中添加

    services.AddSwaggerGen(options =>
    {
        options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
        {
            Version = "v1",
            Title = "DotNet Core WebAPI文档"
        });
    
    });
    

    Startup 类的 Configure 方法中添加

    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "DotNet Core WebAPI文档");
    });
    

    运行效果

    运行 WepAPI 项目,在浏览器中输入 http://localhost:5000/swagger ,效果如下

    参数验证

    此处所说的参数验证指的是实体类型的参数验证,通过在实体的属性上添加特性的方式来实现。

    简单实现

    创建名为 ValidationDemoController 的 API 类,代码如下:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    
    namespace DotNetCoreApiSample.Controllers
    {
        [Route("api/[controller]")]
        public class ValidationDemoController : Controller
        {
            [HttpPost]
           public IActionResult AddUser([FromBody]User user)
           {
                string errorMessage = string.Empty;
                if (!ModelState.IsValid)
                {
                    foreach (var item in ModelState.Values)
                    {
                        foreach (var error in item.Errors)
                        {
                            errorMessage += error.ErrorMessage + "|";
                        }
                    }
                }
                if(!string.IsNullOrEmpty(errorMessage))
                {
                    return BadRequest(errorMessage);
                }
                return Ok();
           }
        }
    
        public class User
        {
            [Required(ErrorMessage = "用户Code不能为空")]
            public string Code { get; set; }
            [Required(ErrorMessage = "用户名称不能为空")]
            public string Name { get; set; }
            [Required(ErrorMessage = "用户年龄不能为空")]
            [Range(1, 100, ErrorMessage = "年龄必须介于1~100之间")]
            public int Age { get; set; }
            public string Address { get; set; }
        }
    }
    
    • 实体类属性使用 Required 等特性需要引用命名空间System.ComponentModel.DataAnnotations
    • 除了上面的 Required 和 Range 标记,还有很多实用的标记,详细参考:https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations(v=vs.110).aspx
    • 上面的示例代码将错误信息的收集写在了接口方法中,这是一个很不好的做法,仅仅实现了功能,下面将通过过滤器的方式来进行重构,统一处理错误信息

    重构

    添加名为 ValidateModelAttribute 的过滤器类,继承 ActionFilterAttribute ,代码如下

    namespace DotNetCoreApiSample.Filters
    {
        public class ValidateModelAttribute : ActionFilterAttribute
        {
            public override void OnActionExecuting(ActionExecutingContext context)
            {
                if (!context.ModelState.IsValid)
                {
                    var result = context.ModelState.Keys
                            .SelectMany(key => context.ModelState[key].Errors.Select(x => new ValidationError(key, x.ErrorMessage)))
                            .ToList();
                    context.Result = new ObjectResult(result);
                }
            }
        }
        public class ValidationError
        {
            [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
            public string Field { get; }
            public string Message { get; }
            public ValidationError(string field, string message)
            {
                Field = field != string.Empty ? field : null;
                Message = message;
            }
        }
    }
    

    Startup 类的 ConfigureServices 方法中添加下面代码:

    services.AddMvc(options =>
    {
        options.Filters.Add<ValidateModelAttribute>();
    });
    

    使用 Postman 调用结果如下

    返回值

    返回值的统一处理需要下面几个步骤:

    • 创建统一返回结果的实体类,所有的接口方法都返回固定格式,方便前端统一处理
    • 创建过滤器,过滤器用来拦截请求,包装结果,统一输出
    • Startup 类中进行配置注册

    结果实体类

    接口的返回值需要统一的格式,下面的属性字段是我认为必须要有的

    • Result:返回的结果
    • Message:出现错误或需要提示时的提示文本内容
    • Code:调用成功、失败或出错时的编码
    • ReturnStatus:用来判断接口调用状态的

    创建返回结果的实体类 BaseResultModel

    public class BaseResultModel
    {
        public BaseResultModel(int? code = null, string message = null,
            object result = null, ReturnStatus returnStatus = ReturnStatus.Success)
        {
            this.Code = code;
            this.Result = result;
            this.Message = message;
            this.ReturnStatus = returnStatus;
        }
        public int? Code { get; set; }
    
        public string Message { get; set; }
    
        public object Result { get; set; }
    
        public ReturnStatus ReturnStatus { get; set; }
    }
    public enum ReturnStatus
    {
        Success = 1,
        Fail = 0,
        ConfirmIsContinue = 2,
        Error = 3
    }
    

    过滤器类

    创建名称为 ApiResultFilterAttribute 的过滤器类,该类继承 ActionFilterAttribute ,具体代码如下

    public class ApiResultFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            base.OnActionExecuting(context);
        }
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var objectResult = context.Result as ObjectResult;
            context.Result = new OkObjectResult(new BaseResultModel(code:200, result: objectResult.Value));
        }
    }
    

    在过滤器中将接口的返回值获取后重新包装到 BaseResultModel 模型类中进行返回。

    Startup 配置

    在 Startup 类的 ConfigureServices 方法中添加如下代码

    services.AddMvc(options =>
    {
        options.Filters.Add<ValidateModelAttribute>();
        options.Filters.Add<ApiResultFilterAttribute>();
    });
    

    添加示例接口方法

    [HttpGet]
    public IActionResult GetUserCode()
    {
        return Ok("oec2003");
    }
    

    运行效果

    使用 Postman 调用该接口方法,返回结果如下

    继续重构参数验证

    添加了返回值的过滤器类后,调用之前的参数验证的接口,会发现返回结果如下

    {
      "code": 200,
      "message": null,
      "result": [
        {
          "field": "Age",
          "message": "年龄必须介于1~100之间"
        }
      ],
      "returnStatus": 1
    }
    

    接口会调用两次过滤器,先调用参数验证的过滤器,再调用返回值的过滤器,导致验证失败的接口返回值状态也是成功的,所以需要做进一步重构。

    1、添加 ValidationFailedResultModel 类

    public class ValidationFailedResultModel : BaseResultModel
    {
        public ValidationFailedResultModel(ModelStateDictionary modelState)
        {
            Code = 422;
            Message = "参数不合法";
            Result = modelState.Keys
                        .SelectMany(key => modelState[key].Errors.Select(x => new ValidationError(key, x.ErrorMessage)))
                        .ToList();
            ReturnStatus = ReturnStatus.Fail;
        }
    }
    
    public class ValidationError
    {
        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
        public string Field { get; }
        public string Message { get; }
        public ValidationError(string field, string message)
        {
            Field = field != string.Empty ? field : null;
            Message = message;
        }
    }
    

    将错误信息的收集移到了 ValidationFailedResultModel 类中,所以
    ValidateModelAttribute 过滤器也需要调整。

    2、修改 ValidateModelAttribute 过滤器,在修改代码之前,先要添加名为 ValidationFailedResult 的类,该类继承 ObjectResult ,用做参数验证的结果收集。

    public class ValidationFailedResult: ObjectResult
    {
    
        public ValidationFailedResult(ModelStateDictionary modelState)
              : base(new ValidationFailedResultModel(modelState))
        {
            StatusCode = StatusCodes.Status422UnprocessableEntity;
        }
    }
    

    修改 ValidateModelAttribute 类

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new ValidationFailedResult(context.ModelState);
        }
    }
    

    3、修改 ApiResultFilterAttribute 过滤器,添加对 ValidationFailedResult 类型的判断

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is ValidationFailedResult)
        {
            var objectResult = context.Result as ObjectResult;
            context.Result = objectResult;
        }
        else
        {
            var objectResult = context.Result as ObjectResult;
            context.Result = new OkObjectResult(new BaseResultModel(code: 200, result: objectResult.Value));
        }
    }
    

    4、调用参数验证接口结果如下

    异常处理

    异常处理和参数验证的方式基本相同,有以下几个步骤

    1、创建名为 CustomExceptionResultModel 的模型类

    public class CustomExceptionResultModel:BaseResultModel
    {
        public CustomExceptionResultModel(int? code, Exception exception)
        {
            Code = code;
            Message = exception.InnerException != null ?
                exception.InnerException.Message :
                exception.Message;
            Result = exception.Message;
            ReturnStatus = ReturnStatus.Error;
        }
    }
    

    2、创建名为 CustomExceptionResult 的异常结果类

    public class CustomExceptionResult:ObjectResult
    {
        public CustomExceptionResult(int? code, Exception exception)
                : base(new CustomExceptionResultModel(code, exception))
        {
            StatusCode = code;
        }
    }
    

    3、创建名为 CustomExceptionAttribute 的异常过滤器类,继承自 IExceptionFilter

    public class CustomExceptionAttribute : IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            HttpStatusCode status = HttpStatusCode.InternalServerError;
    
            //处理各种异常
    
            context.ExceptionHandled = true;
            context.Result = new CustomExceptionResult((int)status, context.Exception);
        }
    }
    

    4、Startup 配置

    在 Startup 类的 ConfigureServices 方法中添加如下代码

    services.AddMvc(options =>
    {
        options.Filters.Add<ValidateModelAttribute>();
        options.Filters.Add<ApiResultFilterAttribute>();
        options.Filters.Add<CustomExceptionAttribute>();
    });
    

    感兴趣的朋友可以在 Github 上下载示例代码进行调试。

    总结

    如果是从零开始搭建一个 WebAPI 项目,这些基础处理是必不可少的,有了这些做保障才能专注于业务代码的编写。

    本文只是抛砖引玉,同样的思路我们还可以实现更多的功能,例如

    • 如果某些特殊接口需要直接返回值怎么办?
    • 怎样记录耗时较长的接口?
    • 怎样做接口的验证?

    点击「阅读原文」可访问示例代码。

  • 相关阅读:
    lua版本的一个状态机
    unity getcomponentsinchildren 翻船
    dotween tips
    ulua c#调用lua中模拟的类成员函数
    洗牌算法
    unity的一些tips
    用Unity写一个12306验证器的恶搞图生成软件
    好久没写Blog了
    蛋疼的时候写三消游戏(十三)
    用DropBox分享Unity3D的Web应用
  • 原文地址:https://www.cnblogs.com/oec2003/p/13121690.html
Copyright © 2020-2023  润新知