包装Action返回结果

许多开发人员喜欢给Action的返回结果再做一层包装,例如:

{
  "code": 1,
  "message": "xxxxxxxx",
  "data": {这里是原本Action的返回结果}
}




效果演示

Nebula对这种需求提供了内置支持,不用考虑这个包装对象的存在,Action只返回该返回的数据就可以了,例如:

[Route("return/namevalue")]
public NameValue ReturnObject2()
{
    return new NameValue("abc", "12345");
}

服务端的返回结果:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
    "code": 1,
    "message": "",
    "detailMessage": "",
    "data": {
        "Name": "abc",
        "Value": "12345"
    }
}

如果遇到异常

[Route("return/error")]
public int ReturnError()
{
    if( DateTime.Now.Year > 2000 )
        throw new MessageException("一个测试消息异常") { StatusCode = 555 };
    else
        return 111;
}

服务端的返回结果:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
    "code": 555,
    "message": "一个测试消息异常",
    "detailMessage": "ex.ToString()",
    "data": ""
}




如何实现?

1,开启一个开头(默认是关闭状态)

在ClownFish.App.config中添加:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="Nebula_WrapDataResultFilter_Enable" value="1" />
    </appSettings>    
</configuration>

或者用环境变量也可以,例如:

docker run -d --name xxxxx  \
  -e Nebula_WrapDataResultFilter_Enable=1 \



2,对返回结果做打包:

先定义包装类型:

public record WrapData(int code, string message, string detailMessage, object data);

再重写 NebulaBehavior.WrapActionResult 方法。

public override object WrapActionResult(object resultOrException)
{
    // Action可能会返回NULL
    if( resultOrException == null) {
        return new WrapData(1, "", "", "");
    }

    // Action可能返回了一个简单的:数字/字符串
    if( resultOrException is string || resultOrException.GetType().IsPrimitive ) {
        return new WrapData(1, "", "", resultOrException.ToString());
    }

    // Action中出现了异常
    if(  resultOrException is Exception ex ) {
        return new WrapData(ex.GetErrorCode(), 
                    ex.Message,
                    "ex.ToString()",   // 实际使用时,不需要用双引号
                    "");
    }

    // Action的“正常”返回数据
    return new WrapData(1, "", "", resultOrException);
}



3,调用API

GET http://localhost:8206/v20/api/WebSiteApp/wrapdata/return/namevalue HTTP/1.1
x-wrap-result: 1

注意上面的调用中,有一个 x-wrap-result 的请求头。

如果没有这个请求头,结果将不会做包装!




谈谈这种API的设计方式

"对Action返回结果再做包装",最初的设计者可能认为(后来人应该是无脑抄):

这种API对调用者会比较友好,因为可以不用考虑异常情况,调用代码会比较简单。

他想像中的调用代码:

var result = call_httpapi(...);
if( errorcode == 1){
   // 调用成功
   // 继续执行其它业务操作
}
else{
   // 调用失败
   // ################ 在这里处理异常 111111111111111
}

国内大厂的许多外部API也确实都采用了这种设计方式,但这种做法其实是 非常差劲的!

因为有些异常情况是不受业务代码控制的,例如:

  • 网络不通
  • 服务不可用
  • 服务端出现不可控异常,例如:OOM

在这些场景下,前面所说的包装方式就无效了,最后调用代码还是必须要增加 try...catch 来捕获异常,例如:

try{
  var result = call_httpapi(...);
  if( errorcode == 1){
    // 调用成功
    // 继续执行其它业务操作
  }
  else{
    // 调用失败
    // ################ 在这里处理异常 111111111111111
  }
}
catch(Exception ex){
    // ################ 这里处理其它类型的异常 222222222222222
}

如果业务项目有全局异常处理机制,errorcode != 1 的场景反而会更麻烦,它需要主动抛出一个异常,来结束当前代码块!

综上所述,这种做法虽然在国内很流行,但是设计想法即是非常愚蠢的!
调用代码不仅得不到简化,反而会更复杂,除非调用方意识不到要做异常处理!