天天向上教育网

因此我开发了YouZack.FromJsonBody这个开源库,让

简介: 因此我开发了YouZack.FromJsonBody这个开源库,让我们可以用这样的方式来进行简单类型参数的绑定:Test([FromJsonBody] int i2,[FromJsonBody("author.age"

本文介绍了一种在ASP.NET Core MVC/ASP.NET Core WebAPI中,将axios等前端提交的json格式请求数据,映射到Action方法的普通类型参数的方法,并且讲解了其实现原理。

一、 为什么要简化json格式请求的参数绑定在ASP.NET Core MVC/ ASP.NET Core WebAPI(以下简称ASP.NET Core)中,可以使用[FromQuery] 从QueryString中获取参数值,也可以使用[FromForm]从表单格式(x-www-form-urlencoded)的请求中获取参数值。

在ASP.NET Core中可以通过[FromBody]来把Action的参数和请求数据绑定在一起。

假如Http请求的内容为:{“UserName”:”test”,”Password”:”123”}那么就要先声明一个包含UserName、Password两个属性的User类,然后再把Action的参数如下声明:public IActionResult Login([FromBody]User u);这样几乎每一个Action方法都要声明一个和请求对应的复杂类,如果项目中Action很多的话,也就会有非常多的“Action参数类”,不胜其烦。

ASP.NET Core对于Json请求,并不能像[FromQuery]一样把Json的某个属性和简单类型的Action参数绑定到一起。

因此我开发了YouZack.FromJsonBody这个开源库,让我们可以用这样的方式来进行简单类型参数的绑定:Test([FromJsonBody] int i2,[FromJsonBody("author.age")]int aAge,[FromJsonBody("author.father.name")] string dadName)这样的Action参数可以直接从如下的Json请求中获取数据:{"i1":1,"i2":5,"author":{"name":"yzk","age":18,"father":{"name":"laoyang","age":28}}}二、 FromJsonBody使用方法这个库使用.NET Standard开发,因此可以支持.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();第三步:在Cooller的Action参数中[FromJsonBody]这个Attribute,参数默认从Json请求的同名的属性中绑定获取值。

如果设定FromJsonBody的PropertyName参数,则从Json请求的PropertyName这个名字的属性中绑定获取值,PropertyName的值也支持[FromJsonBody("author.father.name")]这样的多级属性绑定。

举例1,对于如下的Json请求:{"phoneNumber":"119110","age":3,"salary":333.3,"gender":true,"dir":"west","name":"zack yang"}客户端的请求代码: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');});服务器端Cooller的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,对于如下的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('Send failed');});服务器端Cooller的Action代码:public async Task Post([FromJsonBody("i1")] int i3, [FromJsonBody] int i2,[FromJsonBody("author.age")]int aAge,[FromJsonBody("author.father.name")] string dadName){Debug.WriteLine(aAge);Debug.WriteLine(dadName);return i3 + i2+aAge;}三、 FromJsonBody原理讲解项目的全部代码请参考GitHub地址:https://github.com/yangzhongke/YouZack.FromJsonBodyFromJsonBodyAttribute是一个自定义的数据绑定的Attribute,主要源代码如下:public class FromJsonBodyAttribute : ModelBinderAttribute{public string PropertyName { get; private set; }public FromJsonBodyAttribute(string propertyName=null) : base(typeof(FromJsonBodyBinder)){this.PropertyName = propertyName;}}所有数据绑定Attribute都要继承自ModelBinderAttribute类,当需要尝试计算一个被FromJsonBodyAttribute修饰的参数的绑定值的时候,FromJsonBodyBinder类就会被调用来进行具体的计算。

我们在Startup中调用的UseFromJsonBody()方法就是在应用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;}}}每个Http请求到达服务器的时候,Invoke都会被调用。

为了减少内存占用,默认情况下,ASP.NET Core中对于请求体的数据只能读取一次,不能重复读取。

FromJsonBodyMiddleware需要读取解析请求体的Json,但是后续的ASP.NET Core的其他组件也可能会还要再读取请求体,因此我们通过Request.EnableBuffering()允许请求体的多次读取,这样会对内存占用有轻微的提升。

接下来,使用.NET 新的Json处理库System.Text.Json来进行Json请求的解析:JsonDocument document = JsonDocument.Parse(bodyText)解析完成的Json对象放到context.Items中,供FromJsonBodyBinder使用:context.Items[RequestJsonObject_Key] = jsonRoot下面是FromJsonBodyBinder类的核心代码:public class FromJsonBodyBinder : IModelBinder{public static readonly IDictionary fromJsonBodyAttrCache = new ConcurrentDictionary();public Task BindModelAsync(ModelBindingContext bindingContext){var key = FromJsonBodyMiddleware.RequestJsonObject_Key;object itemValue = 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)){object targetValue = 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 object jsonValue){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 CoollerActionDescriptor;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类的代码做一下分析,当对一个标注了[FromJsonBody]的参数进行绑定的时候,BindModelAsync方法会被调用,绑定的结果(也就是计算后参数的值)要设置到bindingContext.Result中,如果绑定成功就设置:ModelBindingResult.Success(绑定的值),如果因为数据非法等导致绑定失败就设置ModelBindingResult.Failed()在FromJsonBodyBinder类indModelAsync方法中,首先从bindingContext.ActionContext.HttpContext.Items[key]中把FromJsonBodyMiddleware中解析完成的JsonElement取出来。

如果Action有5个参数,那么BindModelAsync就会被调用5次,如果每次BindModelAsync都去做“Json请求体的解析”将会效率比较低,这样在FromJsonBodyMiddleware中提前解析好就可以提升数据绑定的性能。

接下来调用自定义方法GetFromJsonBodyAttr取到方法参数上标注的FromJsonBodyAttribute对象,检测一下FromJsonBodyAttribute上是否设置了PropertyName:如果设置了的话,就用PropertyName做为要绑定的Json的属性名;如果没有设置PropertyName,则用bindingContext.FieldName这个绑定的参数的变量名做为要绑定的Json的属性名。

firstPropName变量就是取出来的” author”, leftPart变量就是剩下的"father.name",然后递归调用ParseJsonValue进一步计算。

非常幸运的是,ASP.NET Core中的ActionDescriptor对象有Id属性,用来获得一个Action方法唯一的标识符,再加上参数的名字,就构成了这个缓存项的Key。

四、 总结Zack. FromJsonBody可以让ASP.NET Core MVC和ASP.NET Core WebAPI程序的普通参数绑定到Http请求的Json报文体中。


以上是文章"

因此我开发了YouZack.FromJsonBody这个开源库,让

"的内容,欢迎阅读天天向上教育网的其它文章