ASP.NET Core 2.2 : 二十. Action的多数据返回格式处理机制
上一章讲了系统如何将客户端提交的请求数据格式化处理成我们想要的格式并绑定到对应的参数,本章讲一下它的“逆过程”,如何将请求结果按照客户端想要的格式返回去。(ASP.NET Core 系列目录)
一、常见的返回类型
以系统模板默认生成的Home/Index这个Action来说,为什么当请求它的时候回返回一个Html页面呢?除了这之外,还有JSON、文本等类型,系统是如何处理这些不同的类型的呢?
首先来说几种常见的返回类型的例子,并用Fiddler请求这几个例子看一下结果,涉及到的一个名为Book的类,代码为:
public class Book
{
public string Code { get; set; }
public string Name { get; set; }
}
1.ViewResult类型
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
返回一个Html页面,Content-Type值为: text/html; charset=utf-8
2.string类型
public string GetString()
{
return "Hello Core";
}
返回字符串“Hello Core”,Content-Type值为:text/plain; charset=utf-8
3.JSON类型
public JsonResult GetJson()
{
return new JsonResult(new Book() { Code = "", Name = "ASP" });
}
返回JSON,值为:
{"code":"1001","name":"ASP"}
Content-Type值为:Content-Type: application/json; charset=utf-8
4.直接返回实体类型
public Book GetModel()
{
return new Book() { Code = "", Name = "ASP" };
}
同样是返回JSON,值为:
{"code":"1001","name":"ASP"}
Content-Type值同样是:Content-Type: application/json; charset=utf-8
5.void类型
public void GetVoid()
{
}
没有返回结果也没有Content-Type值。
下面看几个异步方法:
6.Task类型
public async Task GetTaskNoResult()
{
await Task.Run(() => { });
}
与void类型一样,没有返回结果也没有Content-Type值。
7.Task<string>类型
public async Task<string> GetTaskString()
{
string rtn = string.Empty;
await Task.Run(() => { rtn = "Hello Core"; }); return rtn;
}
与string类型一样,返回字符串“Hello Core”,Content-Type值为:text/plain; charset=utf-8
8.Task<JsonResult>类型
public async Task<JsonResult> GetTaskJson()
{
JsonResult jsonResult = null;
await Task.Run(() => { jsonResult = new JsonResult(new Book() { Code = "", Name = "ASP" }); });
return jsonResult;
}
与JSON类型一样,返回JSON,值为:
[{"code":"1001","name":"ASP"},{"code":"1002","name":"Net Core"}]
Content-Type值为:Content-Type: application/json; charset=utf-8
还有其他类型,这里暂不列举了,总结一下:
- 返回结果有空、Html页面、普通字符串、JSON字符串几种。
- 对应的Content-Type类型有空、text/html、text/plain、application/json几种。
- 异步Action的返回结果,和其对应的同步Action返回结果类型一致。
下一节我们看一下系统是如何处理这些不同的类型的。
二、内部处理机制解析
1.总体流程
通过下图 来看一下总体的流程:
图1
这涉及三部分内容:
第一部分,在invoker的生成阶段。在第14章讲invoker的生成的时候,讲到了Action的执行者的获取,它是从一系列系统定义的XXXResultExecutor中筛选出来的,虽然它们名为XXXResultExecutor,但它们都是Action的执行者而不是ActionResult的执行者,都是ActionMethodExecutor的子类。以Action是同步还是异步以及Action的返回值类型为筛选条件,具体这部分内容见图 14‑2所示XXXResultExecutor列表及其后面的筛选逻辑部分。在图 17‑1中,筛选出了被请求的Action对应的XXXResultExecutor,若以Home/Index这个默认的Action为例,这个XXXResultExecutor应该是SyncActionResultExecutor。
第二部分,在Action Filters的处理阶段。这部分内容见16.5 Filter的执行,此处恰好以Action Filter为例讲了Action Filter的执行方式及Action被执行的过程。在这个阶段,会调用上文筛选出的SyncActionResultExecutor的Execute方法来执行Home/Index这个 Action。执行结果返回一个IActionResult。
第三部分,在Result Filters的处理阶段。这个阶段和Action Filters的逻辑相似,只不过前者的核心是Action的执行,后者的核心是Action的执行结果的执行。二者都分为OnExecuting和OnExecuted两个方法,这两个方法也都在其对应的核心执行方法前后执行。
整体流程是这样,下面看一下细节。
2. ActionMethodExecutor的选择与执行
第一部分,系统为什么要定义这么多种XXXResultExecutor并且在请求的时候一个个筛选合适的XXXResultExecutor呢?从筛选规则是以Action的同步、异步以及Action的返回值类型来看,这么多种XXXResultExecutor就是为了处理不同的Action类型。
依然以Home/Index为例,在筛选XXXResultExecutor的时候,最终返回结果是SyncActionResultExecutor。它的代码如下:
private class SyncActionResultExecutor : ActionMethodExecutor
{
public override ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
var actionResult = (IActionResult)executor.Execute(controller, arguments);
EnsureActionResultNotNull(executor, actionResult);
return new ValueTask<IActionResult>(actionResult);
} protected override bool CanExecute(ObjectMethodExecutor executor)
=> !executor.IsMethodAsync && typeof(IActionResult).IsAssignableFrom(executor.MethodReturnType);
}
XXXResultExecutor的CanExecute方法是筛选的条件,通过这个方法判断它是否适合当前请求的目标Action。它要求这个Action不是异步的并且返回结果类型是派生自IActionResult的。而Home/Index这个Action标识的返回结果是IActionResult,实际是通过View()这个方法返回的,这个方法的返回结果类型实际是IActionResult的派生类ViewResult。这样的派生类还有常见的JsonResult和ContentResult等,他们都继承了ActionResult,而ActionResult实现了IActionResult接口。所以如果一个Action是同步的并且返回结果是JsonResult或ContentResult的时候,对应的XXXResultExecutor也是SyncActionResultExecutor。
第二部分中,Action的执行是在XXXResultExecutor的Execute方法,它会进一步调用了ObjectMethodExecutor的Execute方法。实际上所有的Action的都是由ObjectMethodExecutor的Execute方法来执行执行的。而众多的XXXResultExecutor方法的作用是调用这个方法并且对返回结果进行验证和处理。例如SyncActionResultExecutor会通过EnsureActionResultNotNull方法确保返回的结果不能为空。
如果是sting类型呢?它对应的是SyncObjectResultExecutor,代码如下:
private class SyncObjectResultExecutor : ActionMethodExecutor
{
public override ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
// Sync method returning arbitrary object
var returnValue = executor.Execute(controller, arguments);
var actionResult = ConvertToActionResult(mapper, returnValue, executor.MethodReturnType);
return new ValueTask<IActionResult>(actionResult);
} // Catch-all for sync methods
protected override bool CanExecute(ObjectMethodExecutor executor) => !executor.IsMethodAsync; }
由于string不是IActionResult的子类,所以会通过ConvertToActionResult方法对返回结果returnValue进行处理。
private IActionResult ConvertToActionResult(IActionResultTypeMapper mapper, object returnValue, Type declaredType)
{
var result = (returnValue as IActionResult) ?? mapper.Convert(returnValue, declaredType);
if (result == null)
{
throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(declaredType));
} return result;
}
如果returnValue是IActionResult的子类,则返回returnValue,否则调用一个Convert方法将returnValue转换一下:
public IActionResult Convert(object value, Type returnType)
{
if (returnType == null)
{
throw new ArgumentNullException(nameof(returnType));
} if (value is IConvertToActionResult converter)
{
return converter.Convert();
} return new ObjectResult(value)
{
DeclaredType = returnType,
};
}
这个方法会判断returnValue是否实现了IConvertToActionResult接口,如果是则调用该接口的Convert方法转换成IActionResult类型,否则会将returnValue封装成ObjectResult。ObjectResult也是ActionResult的子类。下文有个ActionResult<T> 类型就是这样,该例会介绍。
所以,针对不同类型的Action,系统设置了多种XXXResultExecutor来处理,最终结果无论是什么,都会被转换成IActionResult类型。方便在图 17‑1所示的第三部分进行IActionResult的执行。
上一节列出了多种不同的Action,它们的处理在这里就不一一讲解了。通过下图 17‑2看一下它们的处理结果:
图 2
这里有void类型没有讲到,它本身没有返回结果,但它会被赋予一个结果EmptyResult,它也是ActionResult的子类。
图 2被两行虚线分隔为三行,第一行基本都介绍过了,第二行是第一行对应的异步方法,上一节介绍常见的返回类的时候说过,这些异步方法的返回结果和对应的同步方法是一样的。不过通过图 2可知,处理它们的XXXResultExecutor方法是不一样的。
第三行的ActionResult<T> 类型是在ASP.NET Core 2.1 引入的,它支持IActionResult的子类也支持类似string和Book这样的特定类型。
public sealed class ActionResult<TValue> : IConvertToActionResult
{
public ActionResult(TValue value)
{
if (typeof(IActionResult).IsAssignableFrom(typeof(TValue)))
{
var error = Resources.FormatInvalidTypeTForActionResultOfT(typeof(TValue), "ActionResult<T>"); throw new ArgumentException(error);
} Value = value;
} public ActionResult(ActionResult result)
{
if (typeof(IActionResult).IsAssignableFrom(typeof(TValue)))
{
var error = Resources.FormatInvalidTypeTForActionResultOfT(typeof(TValue), "ActionResult<T>");
throw new ArgumentException(error);
} Result = result ?? throw new ArgumentNullException(nameof(result));
} /// <summary>
/// Gets the <see cref="ActionResult"/>.
/// </summary>
public ActionResult Result { get; } /// <summary>
/// Gets the value.
/// </summary>
public TValue Value { get; } public static implicit operator ActionResult<TValue>(TValue value)
{
return new ActionResult<TValue>(value);
} public static implicit operator ActionResult<TValue>(ActionResult result)
{
return new ActionResult<TValue>(result);
} IActionResult IConvertToActionResult.Convert()
{
return Result ?? new ObjectResult(Value)
{
DeclaredType = typeof(TValue),
};
}
}
TValue不支持IActionResult及其子类。它的值若是IActionResult子类,会被赋值给Result属性,否则会赋值给Value属性。它实现了IConvertToActionResult接口,想到刚讲解string类型被处理的时候用到的Convert方法。当返回结果实现了IConvertToActionResult这个接口的时候,就会调用它的Convert方法进行转换。它的Convert方法就是先判断它的值是否是IActionResult的子类,如果是则返回该值,否则将该值转换为ObjectResult后返回。
所以图 2中ActionResult<T> 类型返回的结果被加上引号的意思就是结果类型可能是直接返回的IActionResult的子类,也有可能是string和Book这样的特定类型被封装后的ObjectResult类型。
3. Result Filter的执行
结果被统一处理为IActionResult后,进入图 1所示的第三部分。这部分的主要内容有两个,分别是Result Filters的执行和IActionResult的执行。Result Filter也有OnResultExecuting和OnResultExecuted两个方法,分别在IActionResult执行的前后执行。
自定义一个MyResultFilterAttribute,代码如下:
public class MyResultFilterAttribute : Attribute, IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
Debug.WriteLine("HomeController=======>OnResultExecuted");
} public void OnResultExecuting(ResultExecutingContext context)
{
Debug.WriteLine("HomeController=======>OnResultExecuting");
}
}
将它注册到第一节JSON的例子中:
[MyResultFilter]
public JsonResult GetJson()
{
return new JsonResult(new Book() { Code = "", Name = "ASP" });
}
可以看到这样的输出结果:
HomeController=======>OnResultExecuting ……Executing JsonResult…… HomeController=======>OnResultExecuted
在OnResultExecuting中可以通过设置context.Cancel = true;取消后面的工作的执行。
public void OnResultExecuting(ResultExecutingContext context)
{
//用于验证的代码略
context.Cancel = true;
Debug.WriteLine("HomeController=======>OnResultExecuting");
}
再看输出结果就至于的输出了
HomeController=======>OnResultExecuting
同时返回结果也不再是JSON值了,返回结果以及Content-Type全部为空。所以这样设置后,IActionResult以及OnResultExecuted都不再被执行。
在这里除了可以做一些IActionResult执行之前的验证,还可以对HttpContext.Response做一些简单的操作,例如添加个Header值:
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers.Add("version", "1.2");
Debug.WriteLine("HomeController=======>OnResultExecuting");
}
除了正常返回JSON结果外,Header中会出现新添加的version
Content-Type: application/json; charset=utf-
version: 1.2
再看一下OnResultExecuted方法,它是在IActionResult执行之后执行。因为在这个方法执行的时候,请求结果已经发送给请求的客户端了,所以在这里可以做一些日志类的操作。举个例子,假如在这个方法中产生了异常:
public void OnResultExecuted(ResultExecutedContext context)
{
throw new Exception("exception");
Debug.WriteLine("HomeController=======>OnResultExecuted");
}
请求结果依然会返回正常的JSON,但从输出结果中看到是不一样的
HomeController=======>OnResultExecuting
……
System.Exception: exception
发生异常,后面的Debug输出没有 执行。但却将正确的结果返回给了客户端。
Result Filter介绍完了,看一下核心的IActionResult的执行。
4. IActionResult的执行
在ResourceInvoker的case State.ResultInside阶段会调用IActionResult的执行方法InvokeResultAsync。该方法中参数IActionResult result会被调用ExecuteResultAsync方法执行。
protected virtual async Task InvokeResultAsync(IActionResult result)
{
var actionContext = _actionContext;
_diagnosticSource.BeforeActionResult(actionContext, result);
_logger.BeforeExecutingActionResult(result); try
{
await result.ExecuteResultAsync(actionContext);
}
finally
{
_diagnosticSource.AfterActionResult(actionContext, result);
_logger.AfterExecutingActionResult(result);
}
}
由图 2可知,虽然所有类型的Action的结果都被转换成了IActionResult,但它们本质上还是有区别的。所以这个IActionResult类型的参数result实际上可能是JsonResult、ViewResult、EmptyResult等具体类型。下面依然以第一节JSON的例子为例来看,它返回了一个JsonResult。在这里就会调用JsonResult的ExecuteResultAsync方法,JsonResult的代码如下:
public class JsonResult : ActionResult, IStatusCodeActionResult
{
//部分代码略
public override Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} var services = context.HttpContext.RequestServices;
var executor = services.GetRequiredService<JsonResultExecutor>(); return executor.ExecuteAsync(context, this);
}
}
在它的ExecuteResultAsync方法中会获取依赖注入中设置的JsonResultExecutor,由JsonResultExecutor来调用ExecuteAsync方法执行后面的工作。JsonResultExecutor的代码如下:
public class JsonResultExecutor
{
//部分代码略
public virtual async Task ExecuteAsync(ActionContext context, JsonResult result)
{
//验证代码略
var response = context.HttpContext.Response;
ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
result.ContentType,
response.ContentType,
DefaultContentType,
out var resolvedContentType,
out var resolvedContentTypeEncoding); response.ContentType = resolvedContentType; if (result.StatusCode != null)
{
response.StatusCode = result.StatusCode.Value;
} var serializerSettings = result.SerializerSettings ?? Options.SerializerSettings;
Logger.JsonResultExecuting(result.Value);
using (var writer = WriterFactory.CreateWriter(response.Body, resolvedContentTypeEncoding))
{
using (var jsonWriter = new JsonTextWriter(writer))
{
jsonWriter.ArrayPool = _charPool;
jsonWriter.CloseOutput = false;
jsonWriter.AutoCompleteOnClose = false;
var jsonSerializer = JsonSerializer.Create(serializerSettings);
jsonSerializer.Serialize(jsonWriter, result.Value); }
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous write). await writer.FlushAsync();
}
}
}
JsonResultExecutor的ExecuteAsync方法的作用就是将JsonResult中的值转换成JSON串并写入context.HttpContext.Response. Body中。至此JsonResult执行完毕。
ViewResult会有对应的ViewExecutor来执行,会通过相应的规则生成一个 Html页面。
而EmptyResult的ExecuteResult方法为空,所以不会返回任何内容。
public class EmptyResult : ActionResult
{
/// <inheritdoc />
public override void ExecuteResult(ActionContext context)
{ }
}
5. 下集预告
对于以上几种类型返回结果的格式是固定的,JsonResult就会返回JSON格式,ViewResult就会返回Html格式。
但是从第一节的例子可知,string类型会返回string类型的字符串,而Book这样的实体类型却会返回JSON。
由图 2可知这两种类型在执行完毕后,都被封装成了ObjectResult,那么ObjectResult在执行的时候又是如何被转换成string和JSON两种格式的呢?
下一章继续这个话题。
ASP.NET Core 2.2 : 二十. Action的多数据返回格式处理机制的更多相关文章
- ASP.NET Core 2.2 : 二十六. 应用JWT进行用户认证
本文将通过实际的例子来演示如何在ASP.NET Core中应用JWT进行用户认证以及Token的刷新方案(ASP.NET Core 系列目录) 一.什么是JWT? JWT(json web token ...
- ASP.NET Core 2.2 : 二十二. 多样性的配置方式
大多数应用都离不开配置,本章将介绍ASP.NET Core中常见的几种配置方式及系统内部实现的机制. 说到配置,第一印象可能就是“.config”类型的xml文件或者“.ini”类型的ini文件,在A ...
- ASP.NET Core 3.0 : 二十五. TagHelper
什么是TagHelper?这是ASP.NET Core 中新出现的一个名词,它的作用是使服务器端代码可以在Razor 文件中参与创建和呈现HTML 元素.(ASP.NET Core 系列目录) 一.概 ...
- ASP.NET Core 3.0 : 二十四. 配置的Options模式
上一章讲到了配置的用法及内部处理机制,对于配置,ASP.NET Core还提供了一种Options模式.(ASP.NET Core 系列目录) 一.Options的使用 上一章有个配置的绑定的例子,可 ...
- ASP.NET Core 2.2 : 二十六. 应用JWT进行用户认证及Token的刷新
来源:https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_26.html 本文将通过实际的例子来演示如何在ASP.NET Core中应用JWT进行用户认证以及T ...
- ASP.NET Core 3.0 : 二十八. 在Docker中的部署以及docker-compose的使用
本文简要说一下ASP.NET Core 在Docker中部署以及docker-compose的使用 (ASP.NET Core 系列目录). 系统环境为CentOS 8 . 打个广告,求职中.. 一 ...
- ASP.NET Core 2.2 : 二十三. 深入聊一聊配置的内部处理机制
上一章介绍了配置的多种数据源被注册.加载和获取的过程,本节看一下这个过程系统是如何实现的.(ASP.NET Core 系列目录) 一.数据源的注册 在上一节介绍的数据源设置中,appsettings. ...
- 学习ASP.NET Core Razor 编程系列十四——文件上传功能(二)
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十二——在页面中增加校验
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
随机推荐
- 简洁明了的Noip考场策略 / 平时做题也适用
1.选择策略: 评估的标准得分的难度不是AC的难度 2.思考问题: 怀疑的眼光审视自己 3.写代码前: 想想可不可以换一种代码实现会好写很多 把自己的思路再理一遍,可以写到纸上,记下来大致关键顺序 4 ...
- jdk安装及环境配置
1.下载对应的安装包(我们公司用的是jdk 1.8) 2.选择对应版本,点击安装,在选择安装位置的时候,选择自己对应存放的位置,其他都点击下一步就行了,先安装jdk,后安装jre 3.环境变量,选择 ...
- 消息中间件和JMS介绍(一)
在一个公司创立初期,他可能只有几个应用,系统之间的关联也不是那么大,A系统调用B系统就直接调用B提供的API接口:后来这个公司做大了,他一步步发展有了几十个系统,这时候A系统要调用B系统的接口,但是B ...
- Liunx查看后1000行的命令以及查看中间部分
linux 如何显示一个文件的某几行(中间几行) [一]从第3000行开始,显示1000行.即显示3000~3999行 cat filename | tail -n +3000 | head -n 1 ...
- p2p 打洞专场(转)
就像1000个人眼中有1000个哈姆雷特一样,每个人眼中的区块链也是不一样的!作为技术人员眼中的区块链就是将各种技术的融合,包括密码学,p2p网络,分布式共识机制以及博弈论等.我们今天就来讨论一下区块 ...
- Docker之- 使用Docker 镜像和仓库
目录 使用Docker 镜像和仓库 什么是 Docker 镜像 列出 Docker 镜像 tag 标签 Docker Hub 拉取镜像 查找镜像 构建镜像 创建Docker Hub 账号 使用 Doc ...
- centos7 yum搭建lnmp环境及配置wordpress超详细教程
yum安装lnmp环境是最方便,最快捷的一种方法.源码编译安装需要花费大量的人类时间,当然源码编译可以个性化配置一些其它功能.目前来说,yum安装基本满足我们搭建web服务器的需求. 本文是我根据近期 ...
- 最小环-Floyd
floyd求最小环 在Floyd的同时,顺便算出最小环. Floyd算法 :k<=n:k++) { :i<k:i++) :j<k:j++) if(d[i][j]+m[i][k]+m[ ...
- 思科根据键值获取OID方法
一.打开思科网站官网 https://www.cisco.com/ 二.按下图路径选择 三.点击MIB Locator 四.选择"SNMP Object Navigator" 五. ...
- Python变量类型说明
Python中的变量不需要声明,直接赋值便是声明和定义的过程 每个变量在内存中创建,都包括变量的标识.名称和数据这些信息 每个变量在使用前必须赋值 counter = 100 #正数变量 miles ...