ASP.NET Core和json請(qǐng)求這樣用真簡(jiǎn)單,axios、微信小程序得救了
本文介紹了一種在ASP.NET Core MVC/ASP.NET Core WebAPI中,將axios等前端提交的json格式請(qǐng)求數(shù)據(jù),映射到Action方法的普通類型參數(shù)的方法,并且講解了其實(shí)現(xiàn)原理。
一、????? 為什么要簡(jiǎn)化json格式請(qǐng)求的參數(shù)綁定
?在ASP.NET Core MVC/ ASP.NET Core WebAPI(以下簡(jiǎn)稱ASP.NET Core)中,可以使用[FromQuery] 從QueryString中獲取參數(shù)值,也可以使用[FromForm]從表單格式(x-www-form-urlencoded)的請(qǐng)求中獲取參數(shù)值。
隨著前后端分離的流行,現(xiàn)在越來(lái)越多的前端請(qǐng)求體是json格式的,比如非常流行的AJAX前端庫(kù)axios的post請(qǐng)求默認(rèn)就是json格式的,微信小程序的請(qǐng)求也默認(rèn)是json格式的。在ASP.NET Core中可以通過(guò)[FromBody]來(lái)把Action的參數(shù)和請(qǐng)求數(shù)據(jù)綁定在一起。假如Http請(qǐng)求的內(nèi)容為:
{“UserName”:”test”,”Password”:”123”}
那么就要先聲明一個(gè)包含UserName、Password兩個(gè)屬性的User類,然后再把Action的參數(shù)如下聲明:
public IActionResult Login([FromBody]User u);
這樣幾乎每一個(gè)Action方法都要聲明一個(gè)和請(qǐng)求對(duì)應(yīng)的復(fù)雜類,如果項(xiàng)目中Action很多的話,也就會(huì)有非常多的“Action參數(shù)類”,不勝其煩。ASP.NET Core對(duì)于Json請(qǐng)求,并不能像[FromQuery]一樣把Json的某個(gè)屬性和簡(jiǎn)單類型的Action參數(shù)綁定到一起。
因此我開發(fā)了YouZack.FromJsonBody這個(gè)開源庫(kù),讓我們可以用這樣的方式來(lái)進(jìn)行簡(jiǎn)單類型參數(shù)的綁定:
Test([FromJsonBody] int i2,
[FromJsonBody("author.age")]int aAge,
[FromJsonBody("author.father.name")] string dadName)
這樣的Action參數(shù)可以直接從如下的Json請(qǐng)求中獲取數(shù)據(jù):
{"i1":1,"i2":5,"author":{"name":"yzk","age":18,"father":{"name":"laoyang","age":28}}}
二、????? FromJsonBody使用方法
這個(gè)庫(kù)使用.NET Standard開發(fā),因此可以支持.NET Framework及.NET Core,既支持ASP.NET Core MVC,也支持ASP.NET Core Web API。
GitHub地址:https://github.com/yangzhongke/YouZack.FromJsonBody
第一步:
在ASP.NET Core項(xiàng)目中通過(guò)NuGet安裝包:
Install-Package YouZack.FromJsonBody
第二步:
在項(xiàng)目的Startup.cs中添加using YouZack.FromJsonBody;
然后在Configure方法的UseEndpoints()之前添加如下代碼:
app.UseFromJsonBody();
第三步:
在Controller的Action參數(shù)中[FromJsonBody]這個(gè)Attribute,參數(shù)默認(rèn)從Json請(qǐng)求的同名的屬性中綁定獲取值。如果設(shè)定FromJsonBody的PropertyName參數(shù),則從Json請(qǐng)求的PropertyName這個(gè)名字的屬性中綁定獲取值,PropertyName的值也支持[FromJsonBody("author.father.name")]這樣的多級(jí)屬性綁定。
?
舉例1,對(duì)于如下的Json請(qǐng)求:
{"phoneNumber":"119110","age":3,"salary":333.3,"gender":true,"dir":"west","name":"zack yang"}
?客戶端的請(qǐng)求代碼:
axios.post('@Url.Action("Test","Home")',
? ? ? { phoneNumber: "119110", age: 3, salary: 333.3, gender: true,dir:"west",name:"zack yang" })
.then(function (response)
{
? ? ? alert(response.data);
})
.catch(function (error)
{
? ? ? alert('Send failed');
});
服務(wù)器端Controller的Action代碼:
public IActionResult Test([FromJsonBody]string phoneNumber, [FromJsonBody]string test1,
? ? ? [FromJsonBody][Range(0,100,ErrorMessage ="Age must be between 0 and 100")]int? age,
? ? ? [FromJsonBody] bool gender,
? ? ? [FromJsonBody] double salary,[FromJsonBody]DirectionTypes dir,
? ? ? [FromJsonBody][Required]string name)
{
? ? ? if(ModelState.IsValid==false)
? ? ? {
? ? ? ? ? ? ?var errors = ModelState.SelectMany(e => e.Value.Errors).Select(e=>e.ErrorMessage);
? ? ? ? ? ? ?return Json("Invalid input!"+string.Join("\r\n",errors));
? ? ? }
? ? ?return Json($"phoneNumber={phoneNumber},test1={test1},age={age},gender={gender},salary={salary},dir={dir}");
}
舉例2,對(duì)于如下的Json請(qǐng)求:
?
{"i1":1,"i2":5,"author":{"name":"yzk","age":18,"father":{"name":"laoyang","age":28}}}
客戶端的請(qǐng)求代碼:
axios.post('/api/API',
? ? ? { i1: 1, i2: 5, author: { name: 'yzk', age: 18, father: {name:'laoyang',age:28}} })
.then(function (response)
{
? ? ? alert(response.data);
})
.catch(function (error)
{
? ? ? alert('Send failed');
});
服務(wù)器端Controller的Action代碼:
public async Task
三、????? FromJsonBody原理講解
項(xiàng)目的全部代碼請(qǐng)參考GitHub地址:
https://github.com/yangzhongke/YouZack.FromJsonBody
FromJsonBodyAttribute是一個(gè)自定義的數(shù)據(jù)綁定的Attribute,主要源代碼如下:
public class FromJsonBodyAttribute : ModelBinderAttribute
{
? ? ? public string PropertyName { get; private set; }
? ? ? public FromJsonBodyAttribute(string propertyName=null) : base(typeof(FromJsonBodyBinder))
? ? ? {
? ? ? ? ? ? ?this.PropertyName = propertyName;
? ? ? }
}
所有數(shù)據(jù)綁定Attribute都要繼承自ModelBinderAttribute類,當(dāng)需要嘗試計(jì)算一個(gè)被FromJsonBodyAttribute修飾的參數(shù)的綁定值的時(shí)候,F(xiàn)romJsonBodyBinder類就會(huì)被調(diào)用來(lái)進(jìn)行具體的計(jì)算。FromJsonBody這個(gè)庫(kù)的核心代碼都在FromJsonBodyBinder類中。
因?yàn)镕romJsonBodyBinder需要從Json請(qǐng)求體中獲取數(shù)據(jù),為了提升性能,我們編寫了一個(gè)自定義的中間件FromJsonBodyMiddleware來(lái)進(jìn)行Json請(qǐng)求體字符串到解析完成的內(nèi)存對(duì)象JsonDocument,然后把解析完成的JsonDocument對(duì)象供后續(xù)的FromJsonBodyBinder使用。我們?cè)赟tartup中調(diào)用的UseFromJsonBody()方法就是在應(yīng)用FromJsonBodyMiddleware中間件,可以看一下UseFromJsonBody()方法的源代碼如下:
?
public static IApplicationBuilder UseFromJsonBody(this IApplicationBuilder appBuilder)
{
? ? ? return appBuilder.UseMiddleware
如下是FromJsonBodyMiddleware類的主要代碼(全部代碼見Github)
public sealed class FromJsonBodyMiddleware
{
? ? ? public const string RequestJsonObject_Key = "RequestJsonObject";
? ? ? private readonly RequestDelegate _next;
? ? ? public FromJsonBodyMiddleware(RequestDelegate next)
? ? ? {
? ? ? ? ? ? ?_next = next;
? ? ? }
? ? ? public async Task Invoke(HttpContext context)
? ? ? {
? ? ? ? ? ? ?string method = context.Request.Method;
? ? ? ? ? ? ?if (!Helper.ContentTypeIsJson(context, out string charSet)
? ? ? ? ? ? ? ? ? ? ||"GET".Equals(method, StringComparison.OrdinalIgnoreCase))
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? await _next(context);
? ? ? ? ? ? ? ? ? ? return;
? ? ? ? ? ? ?}
? ? ? ? ? ? ?Encoding encoding;
? ? ? ? ? ? ?if(string.IsNullOrWhiteSpace(charSet))
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? encoding = Encoding.UTF8;
? ? ? ? ? ? ?}
? ? ? ? ? ? ?else
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? encoding = Encoding.GetEncoding(charSet);
? ? ? ? ? ? ?} ? ?
? ? ? ? ? ? ?context.Request.EnableBuffering();
? ? ? ? ? ? ?int contentLen = 255;
? ? ? ? ? ? ?if (context.Request.ContentLength != null)
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? contentLen = (int)context.Request.ContentLength;
? ? ? ? ? ? ?}
? ? ? ? ? ? ?Stream body = context.Request.Body;
? ? ? ? ? ? ?string bodyText;
? ? ? ? ? ? ?if(contentLen<=0)
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? bodyText = "";
? ? ? ? ? ? ?}
? ? ? ? ? ? ?else
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? using (StreamReader reader = new StreamReader(body, encoding, true, contentLen, true))
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? ? ?bodyText = await reader.ReadToEndAsync();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ?}
? ? ? ? ? ? ?if(string.IsNullOrWhiteSpace(bodyText))
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? await _next(context);
? ? ? ? ? ? ? ? ? ? return;
? ? ? ? ? ? ?}
? ? ? ? ? ? ?if(!(bodyText.StartsWith("{")&& bodyText.EndsWith("}")))
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? await _next(context);
? ? ? ? ? ? ? ? ? ? return;
? ? ? ? ? ? ?}
? ? ? ? ? ?
? ? ? ? ? ? ?try
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? using (JsonDocument document = JsonDocument.Parse(bodyText))
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? ? ?body.Position = 0;
? ? ? ? ? ? ? ? ? ? ? ? ? ?JsonElement jsonRoot = document.RootElement;
? ? ? ? ? ? ? ? ? ? ? ? ? ?context.Items[RequestJsonObject_Key] = jsonRoot;
? ? ? ? ? ? ? ? ? ? ? ? ? ?await _next(context);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ?}
? ? ? ? ? ? ?catch(JsonException ex)
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? await _next(context);
? ? ? ? ? ? ? ? ? ? return;
? ? ? ? ? ? ?}
? ? ? }
}
每個(gè)Http請(qǐng)求到達(dá)服務(wù)器的時(shí)候,Invoke都會(huì)被調(diào)用。因?yàn)镚et請(qǐng)求一般不帶請(qǐng)求體,所以這里對(duì)于Get請(qǐng)求不處理;同時(shí)對(duì)于請(qǐng)求的ContentType不是application/json的也不處理,這樣可以避免無(wú)關(guān)請(qǐng)求被處理的性能影響。
為了減少內(nèi)存占用,默認(rèn)情況下,ASP.NET Core中對(duì)于請(qǐng)求體的數(shù)據(jù)只能讀取一次,不能重復(fù)讀取。FromJsonBodyMiddleware需要讀取解析請(qǐng)求體的Json,但是后續(xù)的ASP.NET Core的其他組件也可能會(huì)還要再讀取請(qǐng)求體,因此我們通過(guò)Request.EnableBuffering()允許請(qǐng)求體的多次讀取,這樣會(huì)對(duì)內(nèi)存占用有輕微的提升。不過(guò)一般情況下Json請(qǐng)求的請(qǐng)求體都不會(huì)太大,所以這不會(huì)是一個(gè)嚴(yán)重的問(wèn)題。
接下來(lái),使用.NET 新的Json處理庫(kù)System.Text.Json來(lái)進(jìn)行Json請(qǐng)求的解析:
JsonDocument document = JsonDocument.Parse(bodyText)
解析完成的Json對(duì)象放到context.Items中,供FromJsonBodyBinder使用:
context.Items[RequestJsonObject_Key] = jsonRoot
?下面是FromJsonBodyBinder類的核心代碼:
public class FromJsonBodyBinder : IModelBinder
{
? ? ? public static readonly IDictionary
下面對(duì)FromJsonBodyBinder類的代碼做一下分析,當(dāng)對(duì)一個(gè)標(biāo)注了[FromJsonBody]的參數(shù)進(jìn)行綁定的時(shí)候,BindModelAsync方法會(huì)被調(diào)用,綁定的結(jié)果(也就是計(jì)算后參數(shù)的值)要設(shè)置到bindingContext.Result中,如果綁定成功就設(shè)置:ModelBindingResult.Success(綁定的值),如果因?yàn)閿?shù)據(jù)非法等導(dǎo)致綁定失敗就設(shè)置ModelBindingResult.Failed()
在FromJsonBodyBinder類的BindModelAsync方法中,首先從bindingContext.ActionContext.HttpContext.Items[key]中把FromJsonBodyMiddleware中解析完成的JsonElement取出來(lái)。如果Action有5個(gè)參數(shù),那么BindModelAsync就會(huì)被調(diào)用5次,如果每次BindModelAsync都去做“Json請(qǐng)求體的解析”將會(huì)效率比較低,這樣在FromJsonBodyMiddleware中提前解析好就可以提升數(shù)據(jù)綁定的性能。
接下來(lái)調(diào)用自定義方法GetFromJsonBodyAttr取到方法參數(shù)上標(biāo)注的FromJsonBodyAttribute對(duì)象,檢測(cè)一下FromJsonBodyAttribute上是否設(shè)置了PropertyName:如果設(shè)置了的話,就用PropertyName做為要綁定的Json的屬性名;如果沒有設(shè)置PropertyName,則用bindingContext.FieldName這個(gè)綁定的參數(shù)的變量名做為要綁定的Json的屬性名。
接下來(lái)調(diào)用自定義方法ParseJsonValue從Json對(duì)象中取出對(duì)應(yīng)屬性的值,由于從Json對(duì)象中取出來(lái)的數(shù)據(jù)類型可能和參數(shù)的類型不一致,所以需要調(diào)用自定義的擴(kuò)展方法ChangeType()進(jìn)行類型轉(zhuǎn)換。ChangeType方法就是對(duì)Convert.ChangeType的封裝,然后對(duì)于可空類型、枚舉、Guid等特殊類型做了處理,具體到github上看源碼即可。
自定義的ParseJsonValue方法中通過(guò)簡(jiǎn)單的遞歸完成了對(duì)于"author.father.name"這樣多級(jí)Json嵌套的支持。firstPropName變量就是取出來(lái)的” author”, leftPart變量就是剩下的"father.name",然后遞歸調(diào)用ParseJsonValue進(jìn)一步計(jì)算。
自定義的GetFromJsonBodyAttr方法使用反射獲得參數(shù)上標(biāo)注的FromJsonBodyAttribute對(duì)象。為了提升性能,這里把獲取的結(jié)果緩存起來(lái)。非常幸運(yùn)的是,ASP.NET Core中的ActionDescriptor對(duì)象有Id屬性,用來(lái)獲得一個(gè)Action方法唯一的標(biāo)識(shí)符,再加上參數(shù)的名字,就構(gòu)成了這個(gè)緩存項(xiàng)的Key。
?
四、????? 總結(jié)
Zack. FromJsonBody可以讓ASP.NET Core MVC和ASP.NET Core WebAPI程序的普通參數(shù)綁定到Http請(qǐng)求的Json報(bào)文體中。這個(gè)開源項(xiàng)目已經(jīng)被youzack.com這個(gè)英語(yǔ)學(xué)習(xí)網(wǎng)站一年的穩(wěn)定運(yùn)行驗(yàn)證,各位可以放心使用。希望這個(gè)開源項(xiàng)目能夠幫助大家,歡迎使用過(guò)程中反饋問(wèn)題,如果感覺好用,歡迎推薦給其他朋友。