ASP.NET Core和json请求这样用真简单,axios、微信小程序得救了

本文介绍了一种在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)的请求中获取参数值。

随着前后端分离的流行,现在越来越多的前端请求体是json格式的,比如非常流行的AJAX前端库axios的post请求默认就是json格式的,微信小程序的请求也默认是json格式的。在ASP.NET Core中可以通过[FromBody]来把Action的参数和请求数据绑定在一起。假如Http请求的内容为:

{“UserName”:”test”,”Password”:”123”}

那么就要先声明一个包含UserName、Password两个属性的User类,然后再把Action的参数如下声明:

public IActionResultLogin([FromBody]User u);

 这样几乎每一个Action方法都要声明一个和请求对应的复杂类,如果项目中Action很多的话,也就会有非常多的“Action参数类”,不胜其烦。ASP.NET Core对于Json请求,并不能像[FromQuery]一样把Json的某个属性和简单类型的Action参数绑定到一起。

 

因此我开发了YouZack.FromJsonBody这个开源库,让我们可以用这样的方式来进行简单类型参数的绑定:

Test([FromJsonBody] int i2,
[FromJsonBody("author.age")]intaAge,
[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();

 

第三步:

在Controller的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":"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');
});

服务器端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');
});

服务器端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是一个自定义的数据绑定的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类就会被调用来进行具体的计算。FromJsonBody这个库的核心代码都在FromJsonBodyBinder类中。

因为FromJsonBodyBinder需要从Json请求体中获取数据,为了提升性能,我们编写了一个自定义的中间件FromJsonBodyMiddleware来进行Json请求体字符串到解析完成的内存对象JsonDocument,然后把解析完成的JsonDocument对象供后续的FromJsonBodyBinder使用。我们在Startup中调用的UseFromJsonBody()方法就是在应用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请求到达服务器的时候,Invoke都会被调用。因为Get请求一般不带请求体,所以这里对于Get请求不处理;同时对于请求的ContentType不是application/json的也不处理,这样可以避免无关请求被处理的性能影响。

为了减少内存占用,默认情况下,ASP.NETCore中对于请求体的数据只能读取一次,不能重复读取。FromJsonBodyMiddleware需要读取解析请求体的Json,但是后续的ASP.NET Core的其他组件也可能会还要再读取请求体,因此我们通过Request.EnableBuffering()允许请求体的多次读取,这样会对内存占用有轻微的提升。不过一般情况下Json请求的请求体都不会太大,所以这不会是一个严重的问题。

接下来,使用.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<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类的代码做一下分析,当对一个标注了[FromJsonBody]的参数进行绑定的时候,BindModelAsync方法会被调用,绑定的结果(也就是计算后参数的值)要设置到bindingContext.Result中,如果绑定成功就设置:ModelBindingResult.Success(绑定的值),如果因为数据非法等导致绑定失败就设置ModelBindingResult.Failed()

在FromJsonBodyBinder类的BindModelAsync方法中,首先从bindingContext.ActionContext.HttpContext.Items[key]中把FromJsonBodyMiddleware中解析完成的JsonElement取出来。如果Action有5个参数,那么BindModelAsync就会被调用5次,如果每次BindModelAsync都去做“Json请求体的解析”将会效率比较低,这样在FromJsonBodyMiddleware中提前解析好就可以提升数据绑定的性能。

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

接下来调用自定义方法ParseJsonValue从Json对象中取出对应属性的值,由于从Json对象中取出来的数据类型可能和参数的类型不一致,所以需要调用自定义的扩展方法ChangeType()进行类型转换。ChangeType方法就是对Convert.ChangeType的封装,然后对于可空类型、枚举、Guid等特殊类型做了处理,具体到github上看源码即可。

自定义的ParseJsonValue方法中通过简单的递归完成了对于"author.father.name"这样多级Json嵌套的支持。firstPropName变量就是取出来的” author”, leftPart变量就是剩下的"father.name",然后递归调用ParseJsonValue进一步计算。

自定义的GetFromJsonBodyAttr方法使用反射获得参数上标注的FromJsonBodyAttribute对象。为了提升性能,这里把获取的结果缓存起来。非常幸运的是,ASP.NET Core中的ActionDescriptor对象有Id属性,用来获得一个Action方法唯一的标识符,再加上参数的名字,就构成了这个缓存项的Key。

 

四、总结

Zack.FromJsonBody可以让ASP.NET Core MVC和ASP.NET Core WebAPI程序的普通参数绑定到Http请求的Json报文体中。这个开源项目已经被youzack.com这个英语学习网站一年的稳定运行验证,各位可以放心使用。希望这个开源项目能够帮助大家,欢迎使用过程中反馈问题,如果感觉好用,欢迎推荐给其他朋友。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304199.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

10 邮件槽_员工主动发离职邮件,提出申请又反悔,法院判决让人懵了!

前言&#xff1a;很多职场人从来不把劳动法当作一项技能&#xff0c;一遇到事&#xff0c;瞬间就傻。还有部分职场人&#xff0c;什么事都不做&#xff0c;只会说劳动法没有用。就笔者认识的一部分大厂员工&#xff0c;他们现在已经把每天视频打卡跟录音取证作为一项日常工作来…

干货|机器学习零基础?不要怕,吴恩达机器学习课程笔记2-多元线性回归

吴恩达Coursera机器学习课系列笔记课程笔记|吴恩达Coursera机器学习 Week1 笔记-机器学习基础1Linear Regression with Multiple Variables紧接上一篇的例子 – 房价预测。现在我们有更多的特征来预测房价了&#xff0c;“房间的数量”、“楼层”、“房龄”……说明一下接下来要…

技能高考本科计算机类,技能高考多少分上本科

技能高考总分为700分&#xff0c;包括专业技能满分490分&#xff0c;文化课满分210分。能上大学只要总分300往上都可以。如果是本科的话&#xff0c;每个专业的分数线不一样&#xff0c;少的在400分左右&#xff0c;多的比如会计类专业的话可能要到500以上才能报考本科院校。什…

池化对象 RecyclableMemoryStream 在 .netcore 中的使用

Microsoft.IO.RecyclableMemoryStream 是一个被设计为专门用于提高 Stream 操作的高性能类库&#xff0c;意思很明显&#xff0c;专用于取代 MemoryStream 而生&#xff0c;RecyclableMemoryStream 可以最大限度的避免 Stream 操作在 GC 上的 LOH &#xff08;大对象堆&#xf…

ASP.NET中过滤HTML字符串的两个方法

先记下来&#xff0c;以作备用&#xff01;///<summary>去除HTML标记 //////</summary>///<param name"Htmlstring">包括HTML的源码</param>///<returns>已经去除后的文字</returns>publicstaticstringGetNoHTMLString(s…

新增一个主键自增长_MyBatis 示例-主键回填

测试类&#xff1a;com.yjw.demo.PrimaryKeyTest自增长列数据库表的主键为自增长列&#xff0c;在写业务代码的时候&#xff0c;经常需要在表中新增一条数据后&#xff0c;能获得这条数据的主键 ID&#xff0c;MyBatis 提供了实现的方法。StudentMapper.xml<insert id"…

干货|机器学习零基础?不要怕,吴恩达课程笔记第三周!逻辑回归与正则

吴恩达Coursera机器学习课系列笔记课程笔记|吴恩达Coursera机器学习 Week1 笔记-机器学习基础干货|机器学习零基础&#xff1f;不要怕&#xff0c;吴恩达机器学习课程笔记2-多元线性回归1Logistic Regression1.1 Logistic Regression (Classification) Model之前对房价的预测&a…

计算机网络互联网技术实验报告,2013计算机网络技术与应用.实验报告01

本报告 6 月 5 日前完成。 此框阅读后删除。 此处填写&#xff1a;年级和姓名。 《计算机网络技术与应用》实验报告 此框阅读后删除。 年级、专业、班级 实验题目 实验时间 实验成绩 2013.4.1 11 级 专业 班 姓名计算机网络应用软件与拓扑结构实验地点 实验性质 DS1422■验证性…

如何在C#中使用 ArrayPool,MemoryPool

对资源的可复用是提升应用程序性能的一个非常重要的手段&#xff0c;比如本篇要分享的 ArrayPool 和 MemoryPool&#xff0c;它们就有效的减少了内存使用和对GC的压力&#xff0c;从而提升应用程序性能。什么是 ArrayPool System.Buffers 命名空间下提供了一个可对 array 进行复…

LAMP攻略: LAMP环境搭建,Linux下Apache,MySQL,PHP安装与配置

之前写过一个red hat 9下的LAMP环境的配置&#xff0c;不过由于版本比较旧&#xff0c;很多不适用了。 所以决定写一个新的LAMP环境搭建与配置教程。本配置是在 CentOS-5.3 下 httpd-2.2.11.tar.gz MySQL-client-community-5.1.33-0.rhel5.i386.rpm MySQL-devel-community-5.1…

canvas 实现图片局部模糊_Canvas模糊化处理图片、毛玻璃处理图片之stackblur.js

Canvas实现毛玻璃效果解决方式1&#xff1a;使用stackblur.js在Android系统中实现图片的毛玻璃效果比较好用的类库是&#xff1a;Android StackBlur简单API说明&#xff1a;API 调用下面是针对不同的源(图片或者 Canvas 等)进行 StackBlur 的调用。图像作为源:StackBlur.image(…

服务器自动删文件,服务器定时删除文件工具

服务器定时删除文件工具&#xff0c;这是一个定时删除服务器上文件的小程序修改配置文件config.ini&#xff0c;dir是主目录;dirs是要删除文件目录;deltype是删除类型,0是创建日期,1是修改日期;delday是保留天数;deltime是定时删除时间。[config]dir\\cb19\pictifdirs01,02,03,…

30 个实例详解 TOP 命令

Linux中的top命令显示系统上正在运行的进程。它是系统管理员最重要的工具之一。被广泛用于监视服务器的负载。在本篇中&#xff0c;我们会探索top命令的细节。top命令是一个交互命令。在运行top的时候还可以运行很多命令。我们也会探索这些命令。&#xff08;译注&#xff1a;不…

IComparer与IEqualityComparer的简单使用

场景一&#xff1a;对象列表的自定义排序简单类型的列表&#xff0c;可以直接使用Linq的OrderBy或OrderByDescending进行排序&#xff0c;复杂对象的列表排序可以使用Sort()和IComparer实现自定义对象比较规则。假如有一个Box类&#xff0c;它有名称、长、宽、高四个属性&#…

Linux 远程开机(walk on lan)

Linux 远程开机&#xff08;walk on lan&#xff09;一&#xff0c;什么情况下需要远程开机&#xff1f; 如果我们的服务器没有部署在本地&#xff08;实际上通常都是这样的&#xff0c;我们会把服务器托管到IDC机房&#xff09;&#xff0c; 而且服务器在机房中不止一台&am…

程序显示文本框_【教程】TestComplete测试桌面应用程序教程(二)

TestComplete是一款具有人工智能的自动UI测试工具&#xff0c;利用自动化测试工具和人工智能支持的混合对象识别引擎&#xff0c;轻松检测和测试每个桌面&#xff0c;Web和移动应用程序。其中&#xff0c;TestComplete支持测试使用C、C&#xff03;、VB.NET、Java、Delphi、C …

陕西省计算机二级报名流程,计算机二级考试报名流程

计算机二级考试报名流程第一次参加全国计算机等级考试的考生对于网上报名的流程&#xff0c;对全国计算机考试流程中某些环节并不清楚。下面是小编为大家带来的计算机二级考试报名流程&#xff0c;欢迎阅读。(一)注册账号和登录1)考生首次登录系统需要注册登录通行证&#xff0…

Git 的 4 个阶段的撤销更改

虽然git诞生距今已有12年之久&#xff0c;网上各种关于git的介绍文章数不胜数&#xff0c;但是依然有很多人&#xff08;包括我自己在内&#xff09;对于它的功能不能完全掌握。以下的介绍只是基于我个人对于git的理解&#xff0c;并且可能生编硬造了一些不完全符合git说法的词…

51CTO下载专题有奖征集建议:您的期待,我们的方向!

2010年5月10日&#xff0c;51CTO下载专题 隆重上线。精细的技术领域、优质的技术资源、大方的设计风格......让您对精品资源一网打尽&#xff0c;直达心灵所需&#xff01; 51CTO下载专题每周发布1-2期&#xff0c;旨在帮助大家在最短的时间里&#xff0c;找到自己感兴趣技术点…

Visual Studio SnippetDesigner使用

SnippetDesigner代码片段编辑器这是一款在Visual Studio上代码片段编辑器插件&#xff0c;可以轻松创建代码片段&#xff0c;为什么要用这个代码片段呢&#xff0c;理由&#xff1a;平常在编码过程中&#xff0c;有许多重复性的代码语句&#xff0c;为了提高编码速度与便捷&…