主要目標
在Asp.net Core控制器中,通過自定義格式化程序來映射自定義處理控制器中的“未知”內容。
在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;
}
}
為了演示這個問題,我們用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輸入請求開始。
User-Agent: Fiddler
Host: localhost:5000
Content-Type: application/json
Content-Length: 16
請求body:
{"123456"}
通過后台調試和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具有特定內容類型的映射,如果您傳遞的數據不符合這些內容類型,則無法轉換數據,因此它假定沒有匹配的端點可以處理請求。
那么怎么支持原始的請求映射呢?
雖然傳輸json數據是最常用的,但有時候我們需要支持普通的文本或者二進制信息。我們將Content-Type改為
text/plain
User-Agent: Fiddler
Host: localhost:5000
Content-Type:text/plain
Content-Length: 16
請求body:
{"123456"}
悲劇的事情來,報404!
事情到此就變得稍微復雜了一些,因為asp.netcore只處理它認識的類型,如json和formdata。默認情況下,原始數據不能直接映射到控制器參數。這是個小坑,不知你踩到過沒有?仔細想想,這是有道理的。MVC具有特定內容類型的映射,如果您傳遞的數據不符合這些內容類型,則無法轉換數據,因此它假定沒有匹配的端點可以處理請求。
那么怎么支持原始的請求映射呢?
支持原始正文請求
不幸的是,ASP.NET Core不允許您僅通過方法參數以任何有意義的方式捕獲“原始”數據。無論如何,您需要對其進行一些自定義處理Request.Body以獲取原始數據,然后對其進行反序列化。
您可以捕獲原始數據Request.Body並從中直接讀取原始緩沖區。
最簡單,最不易侵入,但不那么明顯的方法是使用一個方法接受沒有參數的 POST或PUT數據,然后從Request.Body以下位置讀取原始數據:
不幸的是,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
要讀取二進制數據,你可以使用以下內容:
[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
}
}
[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();
}
如果你為了方便,寫了很多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那樣自動映射參數的話,你需要做一些自定義格式化適配。
上面雖然解決了原始參數轉換問題,但不夠友好。如果你打算像原生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);
}
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("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
[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
源代碼
示例代碼已上傳到 CsharpFanDemo
示例代碼已上傳到 CsharpFanDemo
參考鏈接
本文包含翻譯和自己實踐。主要思路和代碼來源於以下鏈接:
Accepting Raw Request Body Content in ASP.NET Core API Controllers