主要目标
在 Asp.net Core 控制器中, 通过自定义格式化程序来映射自定义处理控制器中的 "未知" 内容.
简单案例
为了演示这个问题, 我们用 VS2017 创建一个默认的 Asp.net Core web Api 项目.
- [Route("api/[controller]")]
- [ApiController]
- public class ValuesController : ControllerBase{
- [HttpGet]
- public ActionResult<string> Get() {
- return "ok";
- }
- [HttpPost]
- [Route("PostX")]
- public ActionResult<string> Post([FromBody] string value)
- {
- return value;
- }
- }
Json 请求
我们从最常见的 json 输入请求开始.
- User-Agent: Fiddler
- Host: localhost:5000
- Content-Type: application/json
- Content-Length: 16
请求 body:
{"123456"}
通过后台调试和 fiddler 抓包, 我们可以看到请求输入和返回.
后台调试, 查看请求输入结果
fiddler 查看请求 header
fiddler 查看返回结果
注意!!
别忘了[FromBody], 有时候会忘的.
后台 action 接收类型为 string 的时候, 请求 body 只能是字符串, 不能传 json 对象. 我演示这个例子时, 被这点坑了. 如果接收对象是一个类的时候, 才可以传 json 对象.
没有 JSON
虽然传输 json 数据是最常用的, 但有时候我们需要支持普通的文本或者二进制信息. 我们将 Content-Type 改为 text/plain
- User-Agent: Fiddler
- Host: localhost:5000
- Content-Type:text/plain
- Content-Length: 16
请求 body:
{"123456"}
悲剧的事情来, 报 404!
不支持 text/plain
事情到此就变得稍微复杂了一些, 因为 asp.netcore 只处理它认识的类型, 如 json 和 formdata. 默认情况下, 原始数据不能直接映射到控制器参数. 这是个小坑, 不知你踩到过没有? 仔细想想, 这是有道理的. MVC 具有特定内容类型的映射, 如果您传递的数据不符合这些内容类型, 则无法转换数据, 因此它假定没有匹配的端点可以处理请求. 那么怎么支持原始的请求映射呢?
支持原始正文请求
不幸的是, ASP.NET Core 不允许您仅通过方法参数以任何有意义的方式捕获 "原始" 数据. 无论如何, 您需要对其进行一些自定义处理 Request.Body 以获取原始数据, 然后对其进行反序列化.
您可以捕获原始数据 Request.Body 并从中直接读取原始缓冲区.
最简单, 最不易侵入, 但不那么明显的方法是使用一个方法接受没有参数的 POST 或 PUT 数据, 然后从 Request.Body 以下位置读取原始数据:
读取字符串缓冲区
- [HttpPost]
- [Route("PostText")]
- public async Task<string> PostText()
- {
- using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
- {
- return await reader.ReadToEndAsync();
- }
- }
这适用于一下 Http 和文本
- User-Agent: Fiddler
- Host: localhost:5000
- Content-Type: text/plain
- Content-Length: 6
要读取二进制数据, 你可以使用以下内容:
读取 byte 缓冲区
- [HttpPost]
- [Route("PostBinary")]
- public async Task<byte[]> PostBinary()
- {
- using (var ms = new MemoryStream(2048))
- {
- await Request.Body.CopyToAsync(ms);
- return ms.ToArray(); // returns base64 encoded string JSON result
- }
- }
查看执行结果
接收文本内容
接收二进制数据
HttpRequest 静态扩展
如果你为了方便, 写了很多 HttpRequest 的扩展, 接收参数时, 可以看起来更简洁一些.
- public static class HttpRequestExtension
- {
- /// <summary>
- ///
- /// </summary>
- /// <param name="httpRequest"></param>
- /// <param name="encoding"></param>
- /// <returns></returns>
- public static async Task<string> GetRawBodyStringFormater(this HttpRequest httpRequest, Encoding encoding)
- {
- if (encoding == null)
- {
- encoding = Encoding.UTF8;
- }
- using (StreamReader reader = new StreamReader(httpRequest.Body, encoding))
- {
- return await reader.ReadToEndAsync();
- }
- }
- /// <summary>
- /// 二进制
- /// </summary>
- /// <param name="httpRequest"></param>
- /// <param name="encoding"></param>
- /// <returns></returns>
- public static async Task<byte[]> GetRawBodyBinaryFormater(this HttpRequest httpRequest, Encoding encoding)
- {
- if (encoding == null)
- {
- encoding = Encoding.UTF8;
- }
- using (StreamReader reader = new StreamReader(httpRequest.Body, encoding))
- {
- using (var ms = new MemoryStream(2048))
- {
- await httpRequest.Body.CopyToAsync(ms);
- return ms.ToArray(); // returns base64 encoded string JSON result
- }
- }
- }
- }
- [HttpPost]
- [Route("PostTextX")]
- public async Task<string> PostTextX()
- {
- return await Request.GetRawBodyStringAsyn();
- }
- /// <summary>
- /// 接收
- /// </summary>
- /// <returns></returns>
- [HttpPost]
- [Route("PostBinaryX")]
- public async Task<byte[]> PostBinaryX()
- {
- return await Request.GetRawBodyBinaryAsyn();
- }
自动转换文本和二进制值
上面虽然解决了原始参数转换问题, 但不够友好. 如果你打算像原生 MVC 那样自动映射参数的话, 你需要做一些自定义格式化适配.
创建一个 Asp.net MVC InputFormatter
ASP.NET Core 使用一种干净且更通用的方式来处理内容的自定义格式 InputFormatter. 输入格式化程序挂钩到请求处理管道, 让您查看特定类型的内容以确定是否要处理它. 然后, 您可以阅读请求正文并对入站内容执行自己的反序列化. InputFormatter 有几个要求
您需要使用 [FromBody] 去获取
您必须能够查看请求并确定是否以及如何处理内容.
在这个例子中, 对于 "原始内容", 我想查看具有以下类型的请求:
- text/plain(文本)
- appliaction/octet-stream(byte[])
没有内容类型(string)
要创建格式化程序, 你可以实现 IInputFormatter 或者从 InputFormatter 继承.
- public class RawRequestBodyFormatter : IInputFormatter
- {
- public RawRequestBodyFormatter()
- {
- }
- public bool CanRead(InputFormatterContext context)
- {
- if (context == null) throw new ArgumentNullException("argument is Null");
- var contentType = context.HttpContext.Request.ContentType;
- if (string.IsNullOrEmpty(contentType) || contentType == "text/plain" || contentType == "application/octet-stream")
- return true;
- return false;
- }
- public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
- {
- var request = context.HttpContext.Request;
- var contentType = context.HttpContext.Request.ContentType;
- if (string.IsNullOrEmpty(contentType) || contentType.ToLower() == "text/plain")
- {
- using (StreamReader reader = new StreamReader(request.Body, Encoding.UTF8))
- {
- var content = await reader.ReadToEndAsync();
- return await InputFormatterResult.SuccessAsync(content);
- }
- }
- if (contentType == "application/octet-stream")
- {
- using (StreamReader reader = new StreamReader(request.Body, Encoding.UTF8))
- {
- using (var ms = new MemoryStream(2048))
- {
- await request.Body.CopyToAsync(ms);
- var content = ms.ToArray();
- return await InputFormatterResult.SuccessAsync(content);
- }
- }
- }
- return await InputFormatterResult.FailureAsync();
- }
- }
格式化程序用于 CanRead()检查对内容类型的请求以支持, 然后将 ReadRequestBodyAsync()内容读取和反序列化为应在控制器方法的参数中返回的结果类型.
InputFormatter 必须在 ConfigureServices()启动代码中注册 MVC :
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc(o=>o.InputFormatters.Insert(0,new RawRequestBodyFormatter())).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
- }
接受原始输入
- [HttpPost]
- [Route("PostTextPlus")]
- public string PostTextPlus([FromBody] string value)
- {
- return value;
- }
然后你就可以发送 post 请求, 像这样:
- User-Agent: Fiddler
- Host: localhost:5000
- Content-Length: 6
或者
- User-Agent: Fiddler
- Host: localhost:5000
- Content-Type:text/plain
- Content-Length: 6
请注意, 您可以使用内容类型调用相同的控制器方法 application/json 并传递 JSON 字符串, 这也将起作用. 在 RawRequestBodyFormatter 简单地增加它支持的附加内容类型的支持.
二进制数据
- [HttpPost]
- [Route("PostBinaryPlus")]
- public byte[] PostBinaryPlus([FromBody] byte[] value)
- {
- return value;
- }
请求内容如下:
- User-Agent: Fiddler
- Host: localhost:5000
- Content-Length: 6
- Content-Type: application/octet-stream
源代码
示例代码已上传到 https://github.com/fancunwei/CsharpFanDemo.git
来源: http://www.jianshu.com/p/4a96d73428dc