【5min+】美化API,包装AspNetCore的返回结果

系列介绍

【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,好比C#的小细节,AspnetCore,微服务中的.net知识等等。html

经过本篇文章您将Get:前端

  • 将API返回的数据自动包装为所须要的格式
  • 理解AspNetCoreAction返回结果的一系列处理过程

本文的演示代码请点击:Github Linkgit

时长为大约有十分钟,内容丰富,建议先投币再上车观看😜github

正文

当咱们在使用AspNet Core编写控制器的时候,常常会将一个Action的返回结果类型定义为IActionResult,相似于下面的代码:json

[HttpGet]
public IActionResult GetSomeResult()
{
    return OK("My String");
}

当咱们运行起来,经过POSTMan等工具进行调用该API时就会返回My String这样的结果。后端

可是有的时候,您会发现,忽然我忘记将返回类型声明为IActionResult,而是像普通定义方法同样定义Action,就相似下面的代码:api

[HttpGet]
public string GetSomeResult()
{
    return "My String";
}

再次运行,返回结果依旧是同样的。前端框架

那么咱们到底该使用怎样的返回类型呢?Controller里面都有OK()NotFound()Redirect()等方法,这些方法的做用是什么呢? 这些问题都将在下面的内容中获得答案。服务器

合理的定义API返回格式

先回到本文的主题,谈一谈数据返回格式。若是您使用的是WebAPI,那么该问题对您来讲可能更为重要。由于咱们开发出来的API每每是面向的客户端,而客户端一般是由另外的开发人员使用前端框架来开发(好比Vue,Angular,React三巨头)。app

因此开发的时候须要先后两端的人员都遵循某些规则,否则游戏可能就玩不下去了。而API的数据返回格式就是其中的一项。

默认AspNet CoreWebAPI模板实际上是没有特定的返回格式,由于这些业务性质的东西确定是须要开发者本身来定义和完成的。

来感觉一下不使用统一格式的案例场景:

小明(开发人员):我开发了这个API,他将返回用户的姓名:

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

{"name":"张三"}

小丁(前端人员):哦,我知道了,当返回200的时候就是显示姓名吧?那我就把它序列化成JSON对象,而后读取name属性呈现给用户。

小明(开发人员):好的。

五分钟后......

小丁(前端人员): 这是个什么东西?不是说好了返回这个有name的对象吗?

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8
Server: Kestrel

at MiCakeDemoApplication.Controllers.DataWrapperController.NormalExceptionResult() in ……………………(此处省内1000个字符)

小明(开发人员):这个是程序内部报错了嘛,你看结果都是500呀。

小丁(前端人员): 好吧,那我500就不执行操做,而后在界面提醒用户“服务器返回错误”吧。

又过了五分钟......

小丁(前端人员): 那如今是什么状况,返回的是200,可是我又没有办法处理这个对象,致使界面显示了奇奇怪怪的东西。

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

"操做失败,没有检测到该人员"

小明(开发人员):这是由于没有检测到这我的员呀,我就只能返回这个结果。。。

小丁(前端人员): *&&……&#¥%……&(省略N个字)。

上面的场景可能不少开发者都遇到过,由于前期没有构建一个通用的返回模型,致使前端人员不知道应该若是根据返回结果进行序列化和呈现界面。然后端开发者为了图方便,在api中随意返回结果,只负责业务可以调通就OK,可是却没有任何规范。

前端人员此时内心确定有一万只草泥马在奔腾,内心默默吐槽:

这个老几写的啥子歪API哦!

以上内容为:地道四川话

x

所以,咱们须要在API开发初期就协定一个完整的模型,在后期于前端的交互中,你们都遵照这个规范就能够避免这类问题。好比下方这个结构:

{
  "statusCode": 200,
  "isError": false,
  "errorCode": null,
  "message": "Request successful.",
  "result": "{"name":"张三"}"
}

{
  "statusCode": 200,
  "isError": true,
  "errorCode": null,
  "message": "没有找到此人",
  "result": ""
}

当业务执行成功的时候,都将以这种格式进行返回。前端人员能够将该json进行转换,而“result”表明了业务成功时候的结果,而当“isError”为true的时候,表明本次操做业务上存在错误,错误信息会在“message”中显示。

这样当你们都遵循该显示规范的时候,就不会形成前端人员不知道如何反序列结果,致使各类undefined或者null的错误。同时也避免了各类没必要要的沟通成本。

可是后端人员这个时候就很不爽了,我每次都须要返回对应的模型,就像这样:

[HttpGet]
public IActionResult GetSomeResult()
{
    return new DataModel(noError,result,noErrorCode);
}

因此,有没有办法避免这种状况呢? 固然,对结果进行自动包装!!!

AspNet Core中的结果处理流程

在解决这个问题以前,咱们得先来了解一下AspNetCoreAction返回结果以后都经历了哪些过程,这样咱们才能对症下药。

对于通常的Action来讲,好比下面这个返回类型为string的action:

[HttpGet]
public string GetSomeResult()
{
    return "My String";
}

在action结束以后,该返回结果会被包装成为ObjectResultObjectResultAspNetCore里面对于通常结果的经常使用返回类型基类,他继承自IActionResult接口:

public class ObjectResult : ActionResult, IStatusCodeActionResult
{
}

好比返回基础的对象,string、int、list、自定义model等等,都会被包装成为ObjectResult

如下代码来自AspnetCore源码:

//获取action执行结果,好比返回"My String"
var returnValue = await executor.ExecuteAsync(controller, arguments);
//将结果包装为ActionResult
var actionResult = ConvertToActionResult(mapper, returnValue, executor.AsyncResultType);
return actionResult;

//转换过程
private IActionResult ConvertToActionResult(IActionResultTypeMapper mapper, object returnValue, Type declaredType)
{
    //若是已是IActionResult则返回,若是不是则进行转换。
    //咱们例子中返回的是string,显然会进行转换
    var result = (returnValue as IActionResult) ?? mapper.Convert(returnValue, declaredType);
    if (result == null)
    {
        throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(declaredType));
    }

    return result;
}

//实际转换过程
public IActionResult Convert(object value, Type returnType)
{
    if (returnType == null)
    {
        throw new ArgumentNullException(nameof(returnType));
    }

    if (value is IConvertToActionResult converter)
    {
        return converter.Convert();
    }

    //此时string就被包装成为了ObjectResult
    return new ObjectResult(value)
    {
        DeclaredType = returnType,
    };
}

说到这儿就能够提一下我们再初学AspNetCore的时候常常用的OK(xx)方法,它的内部是什么样子的呢?

public virtual OkResult Ok(object value)
            => new OkObjectResult(value);

public class OkObjectResult : ObjectResult
{
}

因此当使用OK()的时候,本质上仍是返回了ObjectResult,这就是为何当咱们使用IActionResult做为Action的返回类型和使用通常类型(好比string)做为返回类型的时候,都会获得一样结果的缘由。

其实这两种写法在大部分场景下都是同样的。因此咱们能够根据本身的爱好书写API

固然,不是全部的状况下,结果都是返回ObjectResult哦,就如同下面这些状况:

  • 当咱们显式返回一个IActionResult的时候
  • 当Action的返回类型为Void,Task等没有返回结果的时候

要记住:AspnetCore的action结果都会被包装为IActionResult,可是ObjectResult只是对IActionResult的其中一种实现。

我在这儿列了一个图,但愿能给你们一个参考:

x

从图中咱们就能够看出,咱们一般在处理一个文件的时候,就不是返回ObjectResult了,而是返回FileResult。还有其它没有返回值的状况,或者身份验证的状况。

可是,对于大部分的状况,咱们都是返回的基础对象,因此都会被包装成为ObjectResult

那么,当返回结果成为了IActionResult以后呢? 是怎么样处理成Http的返回结果的呢?

IActionResult具备一个名为ExecuteResultAsync的方法,该方法用于将对象内容写入到HttpContextHttpResponse中,这样就能够返回给客户端了。

public interface IActionResult
{
    Task ExecuteResultAsync(ActionContext context);
}

每个具体的IActionResult类型,内部都有一个IActionResultExecutor<T>,该Executor实现具体的写入方案。就拿ObjectResult来讲,它内部的Executor是这样的:

public override Task ExecuteResultAsync(ActionContext context)
{
    var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
    return executor.ExecuteAsync(context, this);
}

AspNetCore内置了不少这样的Executor:

services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<PhysicalFileResult>, PhysicalFileResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<VirtualFileResult>, VirtualFileResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<FileStreamResult>, FileStreamResultExecutor>();
and more.....

因此能够看出,具体的实现都是由IActionResultExecutor来完成,咱们拿上面一个稍微简单一点的FileStreamResultExecutor来介绍,它就是将返回的Stream写入到HttpReponse的body中:

public virtual async Task ExecuteAsync(ActionContext context, FileStreamResult result)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (result == null)
    {
        throw new ArgumentNullException(nameof(result));
    }

    using (result.FileStream)
    {
        Logger.ExecutingFileResult(result);

        long? fileLength = null;
        if (result.FileStream.CanSeek)
        {
            fileLength = result.FileStream.Length;
        }

        var (range, rangeLength, serveBody) = SetHeadersAndLog(
            context,
            result,
            fileLength,
            result.EnableRangeProcessing,
            result.LastModified,
            result.EntityTag);

        if (!serveBody)
        {
            return;
        }

        await WriteFileAsync(context, result, range, rangeLength);
    }
}

因此从如今咱们心底就有了一个大体的流程:

  1. Action返回结果
  2. 结果被包裹为IActionResult
  3. IActionResult使用ExecuteResultAsync方法调用属于它的IActionResultExecutor
  4. IActionResultExecutor执行ExecuteAsync方法将结果写入到Http的返回结果中。

这样咱们就从一个Action返回结果到了咱们从POSTMan中看到的结果。

返回结果包装

在有了上面的知识基础以后,咱们就能够考虑怎么样来实现将返回的结果进行自动包装。

结合AspNetCore的管道知识,咱们能够很清楚的绘制出这样的一个流程:

x

图中的Write Data过程就对应上面IActionResult写入过程

因此要包裹Action的结果,咱们大体就有了三种思路:

  1. 经过中间件的方式:在MVC中间件完成后,就能够获得Reponse的结果,而后读取内容,再进行包装。
  2. 经过Filter:在Action执行完成后,会穿事后面的Filter,再把数据写入到Reponse,因此能够利用自定义Filter的方式来进行包装。
  3. AOP:直接对Action进行拦截,返回包装的结果。

该三种方式分别从 起始中间结束 三个时间段来进行操做。也许还有其它的骚操做,可是这里就不说起了。

那么来分析一下这三种方式的优缺点:

  1. 中间件的方式,因为在MVC中间件以后处理,此时获得的数据每每是已经被MVC层写好的结果,多是XML,也多是JSON。因此很难把控到底应该将结果序列化成什么格式。 有时候须要把MVC已经序列化好的数据再次反序列化操做,有没必要要的开销。
  2. Filter方式,可以利用MVC的格式化优点,可是有很小的概率结果可能可能会被其它Filter所冲突掉。
  3. AOP方式:虽然这样作更干脆,可是代理会带来一些成本开销,虽然比较小。

因此最终我我的是比较偏向第二种和第三种方式,可是既然AspNetCore给咱们提供了那么好的Filter,因此就利用Filter的优点来完成的结果包装。

从上面的内容咱们知道了,IActionResult有许许多多的实现类,那么咱们到底该包装哪些结果呢?所有?一部分?

通过考虑以后,我打算仅仅对ObjectResult类型进行包装,由于对于其它的类型来讲,咱们更指望他直接返回结果,好比文件流,重定向结果等等。(你但愿文件流被包装成一个模型吗?😂)

因此很快就会有了下面的一些代码:

internal class DataWrapperFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        if (context.Result is ObjectResult objectResult)
        {
            var statusCode = context.HttpContext.Response.StatusCode;

            var wrappContext = new DataWrapperContext(context.Result,
                                                        context.HttpContext,
                                                        _options,
                                                        context.ActionDescriptor);
            //_wrapperExecutor 负责根据传入的内容进入的内容进行包装
            var wrappedData = _wrapperExecutor.WrapSuccesfullysResult(objectResult.Value, wrappContext);
            //将ObjectResult的Value 替换为包装后的模型类
            objectResult.Value = wrappedData;
            }
        }

        await next();
    }
}


//_wrapperExecutor的方法
public virtual object WrapSuccesfullysResult(object orignalData, DataWrapperContext wrapperContext, bool isSoftException = false)
{
    //other code

    //ApiResponse为咱们定义的格式类型
    return new ApiResponse(ResponseMessage.Success, orignalData) { StatusCode = statuCode };
}

而后将这个Filter交注册到MVC中,访问后的结果就会被包装成咱们须要的格式。

可能有些同窗会问,这个结果是怎么被序列化成json或者xml的,其实在ObjectResultIActionResultExecutor执行过程当中,有一个类型为OutputFormatterSelector的属性,该属性从MVC已经注册了的格式化程序中选择一个最合适的程序把结果写入到Reponse。而MVC给你们内置了stringjson的格式化程序,因此你们默认的返回都是json。若是您要使用xml,则须要在注册时添加xml的支持包。 有关该实现的内容,后面有时间的话能够来写一篇文章单独讲。

总有一些坑

添加自动包装的过滤器的确很简单,我刚开始也是这么认为,特别是我写完初版实现以后,经过调试返回了包装好的int结果的时候。可是,简单的方案可能有不少细节被忽略掉:

永远的statusCode = 200

很快我发现,被包装的结果中httpcode都是200。我很快定位到这一句赋值code的代码:

var statusCode = context.HttpContext.Response.StatusCode;

缘由是IAsyncResultFilter在执行时,context.HttpContext.Response的具体返回内容尚未被写入,因此只会有一个200的值,而真实的返回值如今都还在ObjectResult身上。因此我将代码更改成:

var statusCode = objectResult.StatusCode ?? context.HttpContext.Response.StatusCode;

特殊的结果ProblemDetail

ObjectResultValue属性保存了Action返回的结果数据,好比"123",new MyObject等等。可是在AspNetCore中有一个特殊的类型:ProblemDetail

/// <summary>
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
/// </summary>
public class ProblemDetails
{
    //****
}

该类型是一个规范格式,因此AspNetCore引入了这个类型。因此不少地方都有对该类型进行特殊处理的代码,好比在ObjectResult格式化的时候:

public virtual void OnFormatting(ActionContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (StatusCode.HasValue)
    {
        context.HttpContext.Response.StatusCode = StatusCode.Value;

        if (Value is ProblemDetails details && !details.Status.HasValue)
        {
            details.Status = StatusCode.Value;
        }
    }
}

因此在包装时我开启了一项配置,WrapProblemDetails来提示用户是否对ProblemDetails来进行处理。

ObjectResult的DeclaredType

在最初,我都把注意力放在了ObjectResult的Value属性上,由于当我返回一个类型为int的结果是,它确实成功的包装为了我想要的结果。可是当我返回一个类型为string格式的时候,它抛出了异常。

由于类型为string的结果最终会交给StringOutputFormatter格式化程序进行处理,可是它内部会验证ObjectResult.Value的格式是否为预期,不然就会转换出错。

这是由于在替换ObjectResult的结果时,咱们同时应该替换它的DeclaredType为对应模型的Type:

objectResult.Value = wrappedData;
//This line
objectResult.DeclaredType = wrappedData.GetType();

总结

本次为你们介绍了AspNetCoreAction从返回结果到写入Reponse的过程,在该知识点的基础上咱们很容易就扩展出一个自动包装返回数据的功能来。

在下面的Github连接中,为你们提供了一个数据包装的演示项目。

Github Code:点此跳转

该项目在基础的包装功能上还提供了用户自定义模型的功能,好比:

CustomWrapperModel result = new CustomWrapperModel("MiCakeCustomModel");

result.AddProperty("company", s => "MiCake");
result.AddProperty("statusCode", s => (s.ResultData as ObjectResult)?.StatusCode ?? s.HttpContext.Response.StatusCode);
result.AddProperty("result", s => (s.ResultData as ObjectResult)?.Value);
result.AddProperty("exceptionInfo", s => s.SoftlyException?.Message);

将获得下面的数据格式:

{
  "company": "MiCake",
  "statusCode": 200,
  "result": "There result will be wrapped by micake.",
  "exceptionInfo": null
}

最后,偷偷说一句:创做不易,点个推荐吧.....

x

相关文章
相关标签/搜索