ASP.NET Core和json请求这样用真简单,axios、微信小程序得救了
本文介紹了一種在ASP.NET Core MVC/ASP.NET Core WebAPI中,將axios等前端提交的json格式請求數(shù)據(jù),映射到Action方法的普通類型參數(shù)的方法,并且講解了其實現(xiàn)原理。
?
一、為什么要簡化json格式請求的參數(shù)綁定
????在ASP.NET Core MVC/ ASP.NET Core WebAPI(以下簡稱ASP.NET Core)中,可以使用[FromQuery] 從QueryString中獲取參數(shù)值,也可以使用[FromForm]從表單格式(x-www-form-urlencoded)的請求中獲取參數(shù)值。
隨著前后端分離的流行,現(xiàn)在越來越多的前端請求體是json格式的,比如非常流行的AJAX前端庫axios的post請求默認(rèn)就是json格式的,微信小程序的請求也默認(rèn)是json格式的。在ASP.NET Core中可以通過[FromBody]來把Action的參數(shù)和請求數(shù)據(jù)綁定在一起。假如Http請求的內(nèi)容為:
{“UserName”:”test”,”Password”:”123”}那么就要先聲明一個包含UserName、Password兩個屬性的User類,然后再把Action的參數(shù)如下聲明:
?這樣幾乎每一個Action方法都要聲明一個和請求對應(yīng)的復(fù)雜類,如果項目中Action很多的話,也就會有非常多的“Action參數(shù)類”,不勝其煩。ASP.NET Core對于Json請求,并不能像[FromQuery]一樣把Json的某個屬性和簡單類型的Action參數(shù)綁定到一起。
?
因此我開發(fā)了YouZack.FromJsonBody這個開源庫,讓我們可以用這樣的方式來進(jìn)行簡單類型參數(shù)的綁定:
Test([FromJsonBody] int i2, [FromJsonBody("author.age")]intaAge, [FromJsonBody("author.father.name")]string dadName)?
這樣的Action參數(shù)可以直接從如下的Json請求中獲取數(shù)據(jù):
{"i1":1,"i2":5,"author":{"name":"yzk","age":18,"father":{"name":"laoyang","age":28}}}?
二、FromJsonBody使用方法
這個庫使用.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項目中通過NuGet安裝包:
Install-Package YouZack.FromJsonBody?
第二步:
在項目的Startup.cs中添加using YouZack.FromJsonBody;
然后在Configure方法的UseEndpoints()之前添加如下代碼:
app.UseFromJsonBody();?
第三步:
在Controller的Action參數(shù)中[FromJsonBody]這個Attribute,參數(shù)默認(rèn)從Json請求的同名的屬性中綁定獲取值。如果設(shè)定FromJsonBody的PropertyName參數(shù),則從Json請求的PropertyName這個名字的屬性中綁定獲取值,PropertyName的值也支持[FromJsonBody("author.father.name")]這樣的多級屬性綁定。
?
舉例1,對于如下的Json請求:
{"phoneNumber":"119110","age":3,"salary":333.3,"gender":true,"dir":"west","name":"zackyang"}?
客戶端的請求代碼:
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('Sendfailed'); });服務(wù)器端Controller的Action代碼:
public IActionResultTest([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]stringname) {if(ModelState.IsValid==false){varerrors = ModelState.SelectMany(e =>e.Value.Errors).Select(e=>e.ErrorMessage);returnJson("Invalid input!"+string.Join("\r\n",errors));}returnJson($"phoneNumber={phoneNumber},test1={test1},age={age},gender={gender},salary={salary},dir={dir}"); }?
舉例2,對于如下的Json請求:
?
{"i1":1,"i2":5,"author":{"name":"yzk","age":18,"father":{"name":"laoyang","age":28}}}?
客戶端的請求代碼:
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('Sendfailed'); });服務(wù)器端Controller的Action代碼:
public async Task<int>Post([FromJsonBody("i1")] int i3, [FromJsonBody] int i2,[FromJsonBody("author.age")]intaAge,[FromJsonBody("author.father.name")] string dadName) {Debug.WriteLine(aAge);Debug.WriteLine(dadName);returni3 + i2+aAge; }??
三、FromJsonBody原理講解
項目的全部代碼請參考GitHub地址:
https://github.com/yangzhongke/YouZack.FromJsonBody
FromJsonBodyAttribute是一個自定義的數(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)需要嘗試計算一個被FromJsonBodyAttribute修飾的參數(shù)的綁定值的時候,FromJsonBodyBinder類就會被調(diào)用來進(jìn)行具體的計算。FromJsonBody這個庫的核心代碼都在FromJsonBodyBinder類中。
因為FromJsonBodyBinder需要從Json請求體中獲取數(shù)據(jù),為了提升性能,我們編寫了一個自定義的中間件FromJsonBodyMiddleware來進(jìn)行Json請求體字符串到解析完成的內(nèi)存對象JsonDocument,然后把解析完成的JsonDocument對象供后續(xù)的FromJsonBodyBinder使用。我們在Startup中調(diào)用的UseFromJsonBody()方法就是在應(yīng)用FromJsonBodyMiddleware中間件,可以看一下UseFromJsonBody()方法的源代碼如下:
?
public static IApplicationBuilderUseFromJsonBody(this IApplicationBuilder appBuilder) {return appBuilder.UseMiddleware<FromJsonBodyMiddleware>(); }如下是FromJsonBodyMiddleware類的主要代碼(全部代碼見Github)
public sealed class FromJsonBodyMiddleware {public const string RequestJsonObject_Key = "RequestJsonObject";private readonly RequestDelegate _next;public FromJsonBodyMiddleware(RequestDelegate next){_next= next;}publicasync 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;}Streambody = 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;JsonElementjsonRoot = document.RootElement;context.Items[RequestJsonObject_Key]= jsonRoot;await _next(context);}}catch(JsonExceptionex){await _next(context);return;}} }每個Http請求到達(dá)服務(wù)器的時候,Invoke都會被調(diào)用。因為Get請求一般不帶請求體,所以這里對于Get請求不處理;同時對于請求的ContentType不是application/json的也不處理,這樣可以避免無關(guān)請求被處理的性能影響。
為了減少內(nèi)存占用,默認(rèn)情況下,ASP.NETCore中對于請求體的數(shù)據(jù)只能讀取一次,不能重復(fù)讀取。FromJsonBodyMiddleware需要讀取解析請求體的Json,但是后續(xù)的ASP.NET Core的其他組件也可能會還要再讀取請求體,因此我們通過Request.EnableBuffering()允許請求體的多次讀取,這樣會對內(nèi)存占用有輕微的提升。不過一般情況下Json請求的請求體都不會太大,所以這不會是一個嚴(yán)重的問題。
接下來,使用.NET 新的Json處理庫System.Text.Json來進(jìn)行Json請求的解析:
JsonDocument document =JsonDocument.Parse(bodyText)解析完成的Json對象放到context.Items中,供FromJsonBodyBinder使用:
context.Items[RequestJsonObject_Key]= jsonRoot?
下面是FromJsonBodyBinder類的核心代碼:
public class FromJsonBodyBinder :IModelBinder {public static readonly IDictionary<string, FromJsonBodyAttribute> fromJsonBodyAttrCache = new ConcurrentDictionary<string,FromJsonBodyAttribute>();public Task BindModelAsync(ModelBindingContext bindingContext){var key = FromJsonBodyMiddleware.RequestJsonObject_Key;objectitemValue =bindingContext.ActionContext.HttpContext.Items[key];JsonElement jsonObj =(JsonElement)itemValue;string fieldName = bindingContext.FieldName;FromJsonBodyAttribute fromJsonBodyAttr =GetFromJsonBodyAttr(bindingContext, fieldName);if(!string.IsNullOrWhiteSpace(fromJsonBodyAttr.PropertyName)){fieldName =fromJsonBodyAttr.PropertyName;}object jsonValue;if(ParseJsonValue(jsonObj, fieldName, out jsonValue)){objecttargetValue =jsonValue.ChangeType(bindingContext.ModelType);bindingContext.Result=ModelBindingResult.Success(targetValue);}else{bindingContext.Result= ModelBindingResult.Failed();}return Task.CompletedTask;}private static bool ParseJsonValue(JsonElement jsonObj, string fieldName, out objectjsonValue){int firstDotIndex = fieldName.IndexOf('.');if(firstDotIndex>=0){string firstPropName = fieldName.Substring(0, firstDotIndex);string leftPart = fieldName.Substring(firstDotIndex + 1);if(jsonObj.TryGetProperty(firstPropName,out JsonElement firstElement)){return ParseJsonValue(firstElement, leftPart, out jsonValue);}else{jsonValue= null;return false;}}else{bool b = jsonObj.TryGetProperty(fieldName, out JsonElement jsonProperty);if(b){jsonValue= jsonProperty.GetValue();}else{jsonValue= null;}return b;} }private static FromJsonBodyAttribute GetFromJsonBodyAttr(ModelBindingContext bindingContext, string fieldName){var actionDesc = bindingContext.ActionContext.ActionDescriptor;string actionId = actionDesc.Id;string cacheKey = $"{actionId}:{fieldName}";FromJsonBodyAttribute fromJsonBodyAttr;if(!fromJsonBodyAttrCache.TryGetValue(cacheKey, out fromJsonBodyAttr)){var ctrlActionDesc =bindingContext.ActionContext.ActionDescriptor as ControllerActionDescriptor;var fieldParameter =ctrlActionDesc.MethodInfo.GetParameters().Single(p =>p.Name == fieldName);fromJsonBodyAttr=fieldParameter.GetCustomAttributes(typeof(FromJsonBodyAttribute),false) .Single() as FromJsonBodyAttribute;fromJsonBodyAttrCache[cacheKey]= fromJsonBodyAttr;} return fromJsonBodyAttr;} }下面對FromJsonBodyBinder類的代碼做一下分析,當(dāng)對一個標(biāo)注了[FromJsonBody]的參數(shù)進(jìn)行綁定的時候,BindModelAsync方法會被調(diào)用,綁定的結(jié)果(也就是計算后參數(shù)的值)要設(shè)置到bindingContext.Result中,如果綁定成功就設(shè)置:ModelBindingResult.Success(綁定的值),如果因為數(shù)據(jù)非法等導(dǎo)致綁定失敗就設(shè)置ModelBindingResult.Failed()
在FromJsonBodyBinder類的BindModelAsync方法中,首先從bindingContext.ActionContext.HttpContext.Items[key]中把FromJsonBodyMiddleware中解析完成的JsonElement取出來。如果Action有5個參數(shù),那么BindModelAsync就會被調(diào)用5次,如果每次BindModelAsync都去做“Json請求體的解析”將會效率比較低,這樣在FromJsonBodyMiddleware中提前解析好就可以提升數(shù)據(jù)綁定的性能。
接下來調(diào)用自定義方法GetFromJsonBodyAttr取到方法參數(shù)上標(biāo)注的FromJsonBodyAttribute對象,檢測一下FromJsonBodyAttribute上是否設(shè)置了PropertyName:如果設(shè)置了的話,就用PropertyName做為要綁定的Json的屬性名;如果沒有設(shè)置PropertyName,則用bindingContext.FieldName這個綁定的參數(shù)的變量名做為要綁定的Json的屬性名。
接下來調(diào)用自定義方法ParseJsonValue從Json對象中取出對應(yīng)屬性的值,由于從Json對象中取出來的數(shù)據(jù)類型可能和參數(shù)的類型不一致,所以需要調(diào)用自定義的擴(kuò)展方法ChangeType()進(jìn)行類型轉(zhuǎn)換。ChangeType方法就是對Convert.ChangeType的封裝,然后對于可空類型、枚舉、Guid等特殊類型做了處理,具體到github上看源碼即可。
自定義的ParseJsonValue方法中通過簡單的遞歸完成了對于"author.father.name"這樣多級Json嵌套的支持。firstPropName變量就是取出來的” author”, leftPart變量就是剩下的"father.name",然后遞歸調(diào)用ParseJsonValue進(jìn)一步計算。
自定義的GetFromJsonBodyAttr方法使用反射獲得參數(shù)上標(biāo)注的FromJsonBodyAttribute對象。為了提升性能,這里把獲取的結(jié)果緩存起來。非常幸運的是,ASP.NET Core中的ActionDescriptor對象有Id屬性,用來獲得一個Action方法唯一的標(biāo)識符,再加上參數(shù)的名字,就構(gòu)成了這個緩存項的Key。
?
四、總結(jié)
Zack.FromJsonBody可以讓ASP.NET Core MVC和ASP.NET Core WebAPI程序的普通參數(shù)綁定到Http請求的Json報文體中。這個開源項目已經(jīng)被youzack.com這個英語學(xué)習(xí)網(wǎng)站一年的穩(wěn)定運行驗證,各位可以放心使用。希望這個開源項目能夠幫助大家,歡迎使用過程中反饋問題,如果感覺好用,歡迎推薦給其他朋友。
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core和json请求这样用真简单,axios、微信小程序得救了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iNeuOS工业互联操作系统,图表与数据
- 下一篇: 池化对象 RecyclableMemor