asp.net core 實現支持自定義 Content-Type
Intro
我們最近有一個原本是內網的服務要上公網,在公網上有一層 Cloudflare
作為網站的公網流量提供者,CloudFlare 會有一層防火牆攔截掉一些非法的請求,我們有一些 API 會提交一些 html 內容,經過 Cloudflare
的時候會被 Cloudflare
攔截,導致某些功能不能夠正常使用,於是就想對提交的數據進行一個編碼之后再提交,服務器端針對需要解碼的請求進行解碼再解析,我們新加了一個 Content-Type
的支持,編碼后的數據使用新的 Content-Type
,對於不編碼的數據依然可以工作,目前我們做了一個簡單的 base64 編碼,如果需要的話也可以實現復雜一些的加密、壓縮等。
Basis
asp.net core 默認支持 JSON 請求,因為內置了針對 JSON 內容的 Formatter
,.NET Core 2.x 使用的是 Newtonsoft.Json
作為默認 JSON formatter,從 .NET Core 3.0 開始引入了 System.Text.Json
作為默認的 JSON formatter,如果要支持 XML 需要引入針對 XML 的 formatter,相應的如果需要增加其他類型的請求實現自己的 formatter 就可以了
Formatter 分為 InputFormatter
和 OutputFormatter
InputFormatter
用來解析請求Body
的數據,將請求參數映射到強類型的 model,Request Body => ValueOutputFormatter
用來將強類型的數據序列化成響應輸出,Value => Response Body
Formatter 需要指定支持的 MediaType
,可以理解為請求類型,體現在請求頭上,對於 InputFormatter
對應的就是 Content-Type
,對於 OutputFormatter
對應的是 Accept
,asp.net core 會根據請求信息來選擇注冊的 formatter。
Sample
先來看一下實現效果吧,實現效果如下:
swagger 的支持也算比較好了,在增加了新的 Content-Type
支持之后在 swagger 上可以看得到,而且可以切換請求的 Content-Type
,上圖中的 text/base64-json
就是我自定義的一個 Content-Type
默認請求:
對原始請求進行 base64 編碼,再請求:
Implement
實現代碼如下:
public class Base64EncodedJsonInputFormatter : TextInputFormatter
{
public Base64EncodedJsonInputFormatter()
{
// 注冊支持的 Content-Type
SupportedMediaTypes.Add("text/base64-json");
SupportedEncodings.Add(Encoding.UTF8);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
try
{
using var reader = context.ReaderFactory(context.HttpContext.Request.Body, encoding);
var rawContent = await reader.ReadToEndAsync();
if (string.IsNullOrEmpty(rawContent))
{
return await InputFormatterResult.NoValueAsync();
}
var bytes = Convert.FromBase64String(rawContent);
var services = context.HttpContext.RequestServices;
var modelValue = await GetModelValue(services, bytes);
return await InputFormatterResult.SuccessAsync(modelValue);
async ValueTask<object> GetModelValue(IServiceProvider serviceProvider, byte[] stringBytes)
{
var newtonJsonOption = serviceProvider.GetService<IOptions<MvcNewtonsoftJsonOptions>>()?.Value;
if (newtonJsonOption is null)
{
await using var stream = new MemoryStream(stringBytes);
var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, context.ModelType,
services.GetRequiredService<IOptions<JsonOptions>>().Value.JsonSerializerOptions);
return result;
}
var stringContent = encoding.GetString(bytes);
return Newtonsoft.Json.JsonConvert.DeserializeObject(stringContent, context.ModelType, newtonJsonOption.SerializerSettings);
}
}
catch (Exception e)
{
context.ModelState.TryAddModelError(string.Empty, e.Message);
return await InputFormatterResult.FailureAsync();
}
}
}
上述代碼兼容了使用 System.Text.Json
和 Newtonsoft.Json
,在發生異常時將錯誤信息添加一個 ModelError
以便在前端可以得到錯誤信息的反饋,例如傳一個不合法的 base64 字符串就會像下面這樣:
實際使用的時候,只需要在 Startup
里配置一下就可以了,如:
services.AddControllers(options =>
{
options.InputFormatters.Add(new Base64EncodedJsonInputFormatter());
});
More
通過自定義 Content-Type
的支持我們可以無侵入的實現不同的請求內容,上面的示例代碼可以在 Github 上獲取 https://github.com/WeihanLi/SamplesInPractice/tree/master/AspNetCoreSample,可以根據自己的需要進行自定義