ASP.NET WebAPI 2 - 使用 POST Body 傳送多參數(NSwag 版)
2 |
前篇文章實踐以 NSwag 取代 Swashbuckle 為 ASP.NET WebAPI 產生 Swagger 文件及 Swagger UI 線上測試介面的第一步。接著面對之前 Swashbuckle 遇過的老問題 - 如何改以 POST Body 方式傳遞參數,以避免 Query String 參數外露衍生風險,為什麼要讓 ASP.NET WebAPI 2 改用 POST Body 的原理細節請參考前文,本文將聚焦如何改用 NSwag 實現。
首先,為讓 ApiController 比照 ASP.NET MVC 同時支援 Query String 和 Body (application/x-www-form-urlencoded) 取值,一樣需安裝 WebApiContrib NuGet 套件,並在 Controller 加註 [MvcStyleBinding]。
依上回修改 Swashbuckle 的經驗,要將參數定義以 application/x-www-form-urlencoded 傳送,需將 Swagger.json Operation Consumes 修改為 application/x-www-form-urlencoded 並將參數 SwaggerParameterKind 改為 formData。這方面一樣是 ASP.NET Core 支援較完整,直接在 Action 加上 [Consumes("application/x-www-form-urlencoded")],在參數加上 ASP.NET Core Attribute [FromForm] 就可搞定。若是 ASP.NET WebAPI 2,並沒有這些 Attribute 並不存在,所幸查過原始碼發現 NSwag 產生文件時是以 Attribute 名稱識別配合 dynamic 控制 Consumes 及 SwaggerParameterKind,並不依賴強型別物件,這給了一個很簡單的切入點,想辦法自己做一個同名 Attribute 型別,一樣可以產生效果。
所以我在 Models 目錄下新增兩個自訂 Attribute。ConsumesAttribute.cs
using System;
namespace Microsoft.AspNetCore.Mvc
{
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ConsumesAttribute : Attribute
{
public string[] ContentTypes { get; set; }
public ConsumesAttribute(params string[] contentTypes)
{
ContentTypes = contentTypes;
}
}
}
FromFormAttribute.cs
using System;
namespace Microsoft.AspNetCore.Mvc
{
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromFormAttribute : Attribute
{
public string Name { get; set; }
}
}
接著要改寫 CodecController.cs,主要修改點為:
- CodecController 加上 [MvcStyleBinding]
- EncryptString() 欲改為 POST Body 傳值,故要加上 [Consumes("application/x-www-form-urlencoded")]
- encKey, rawText 兩個參數加上 [FromForm]
using System.Collections.Generic;
using System.Web.Http;
using Microsoft.AspNetCore.Mvc;
using WebApiContrib.ModelBinders;
using WebApiDemo.Models;
namespace WebApiDemo.Controllers
{
/// <summary>
/// 加解密功能
/// </summary>
[MvcStyleBinding]
public class CodecController : ApiController
{
/// <summary>
/// 加密字串
/// </summary>
/// <param name="encKey">加密金鑰</param>
/// <param name="rawText">明文字串</param>
/// <returns>加密字串</returns>
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public byte[] EncryptString([FromForm]string encKey, [FromForm]string rawText)
{
return CodecModule.EncrytString(encKey, rawText);
}
/// <summary>
/// 解密請求參數物件
/// </summary>
public class DecryptParameter
{
/// <summary>
/// 加密金鑰
/// </summary>
public string EncKey { get; set; }
/// <summary>
/// 加密字串陣列
/// </summary>
public List<byte[]> EncData { get; set; }
}
/// <summary>
/// 批次解密
/// </summary>
/// <param name="decData">解密請求參數(加解密金鑰與加密字串陣列)</param>
/// <returns>解密字串陣列</returns>
[HttpPost]
public List<string> BatchDecryptData([FromBody]DecryptParameter decData)
{
return CodecModule.DecryptData(decData.EncKey, decData.EncData);
}
}
}
調整後,由 Swagger UI 可驗證我們已成功將 EncryptString() 改造為 application/x-www-form-urlencoded 傳送,而 encKey 與 rawText 參數屬性也已調為 formData,由下方 Curl 範例也可確認參數是以 POST Body UrlEncoded 方式傳送。
不過,Controller 套用 [MvcStyleBinding] 有後遺症,原本 BatchDecryptData() 使用 [FromBody] 讀取 DecryptParameter 型別參數,在套用 MvcStyleBinding 後失效,出現以下錯誤:
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Net.Http.Formatting.FormDataCollection' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.\r\nTo fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List
) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.\r\nPath 'EncKey', line 2, position 11.
深入調查發現問題出在 MvcStyleBinding 背後是靠 MvcActionBinding 處理參數繫結,其邏輯未考慮 [FromBody],一律將 POST 內容解析成 FormDataCollection 型別。參考 Stackoverflow 討論,我也決定從修改 MvcActionBinding 邏輯下手,加一段客製邏輯,當參數被標註 [FromBody] 時,改將 POST 內容反序列化為該參數型別:
public override Task ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
HttpRequestMessage request = actionContext.ControllerContext.Request;
HttpContent content = request.Content;
if (content != null)
{
HttpParameterDescriptor fromBodyParam = null;
//偵測是否有任何參數有[FromBody]
if ((fromBodyParam = actionContext.ActionDescriptor
.GetParameters().SingleOrDefault(o => o.GetCustomAttributes<FromBodyAttribute>().Any())) != null)
{
//若有,將Content內容JSON反列化為參數型別
var json = content.ReadAsStringAsync().Result;
var value = JsonConvert.DeserializeObject(json, fromBodyParam.ParameterType);
var vp = new NameValuePairsValueProvider(new Dictionary<string, object>()
{
[fromBodyParam.ParameterName] = value
}, CultureInfo.CurrentCulture);
request.Properties.Add(Key, vp);
}
else
{
FormDataCollection fd = content.ReadAsAsync<FormDataCollection>().Result;
if (fd != null)
{
IValueProvider vp = new NameValuePairsValueProvider(fd, CultureInfo.InvariantCulture);
request.Properties.Add(Key, vp);
}
}
}
return base.ExecuteBindingAsync(actionContext, cancellationToken);
}
修改後,[MvcStyleBinding] 與 [FromBody] 就能並存了。
Tips of how to setup ASP.NET WebAPI2 with NSwag to accept appliation/x-www-form-urlencoded content type parameters from post body.
Comments
# by Cloud
前篇文章實踐以 NSwag 取代 Swashbuckle <--- 前篇文章的連結 404
# by Jeffrey
to Cloud, 謝謝,已更正。