Web APi之捕获请求原始内容的实现方法以及接受POST请求多个参数多种解决方案(十四)
前言
我们知道在Web APi中捕获原始请求的内容是肯定是很容易的,但是这句话并不是完全正确,前面我们是不是讨论过,在Web APi中,如果对于字符串发出非Get请求我们则会出错,为何?因为Web APi对于简单的值不能很好的映射。之前我们谈论过请求内容注意事项问题,本节我们将更加深入的来讨论这个问题,我们会循序渐进进行探讨,并给出可行的解决方案,。细细品,定让你收货多多!
捕获复杂属性值
Web APi对于复杂属性值以JSON或者XML的形式成功发送到服务器,基于这点是非常容易而且简单的,如果我们想捕获一个对象,我们只需简单的创建一个控制并在其方法上有一个对象参数即可,因为Web APi会自动以解码JSON或者XML的处理形式到控制器上的方法参数对象中,如下:
[HttpPost]
public HttpResponseMessage PostPerson(Person person)
{
}
对于上述我们不需要获得person并进行解析,Web APi内部会自动检测content type,并将其映射到MediaFormatter媒体格式并将其转换为JSON或者XML格式,或者说我们配置的其他类型,并将其转换为对应的格式。
如果我们是发出POST请求的表单数据,且表单数据以键值对的形式进行编码,此时Web APi会利用模型绑定将其表单的键映射到对象的属性中,所以由上知,对于复杂类型的映射那将是非常简单的,这点和MVC模型绑定类似,以上就是复杂类型映射的一部分。接着我们将继续进行讨论,请往下看。
捕获原始请求内容
对于这个请求却不如上述复杂类型的映射那么简单并且透明,例如,当我们想要通过简单的参数如string、 number、DateTime等等。都说复杂的并不复杂,简单的反而不简单,从这里看出,老外是不是也吸取了这句话的精华呢。因为Web APi是基于宿主约定,对于一些通过POST或者PUT请求的操作来捕获其值,这是很容易的,但是就如以上复杂类型它不会进行自动检测其类型进行映射,而且是不透明的。
我们可能会进行如下操作,并且认为结果会如我们所料,我们会认为获取其值并进行映射到方法上的参数中。
[HttpPost]
public string PostRawContent(string content)
{
return content;
}
如上,最终没能如我们所愿,并且还给我们任何提示,为何?因为此方法的参数签名是有问题的。我们就不演示了,我们这里可以总结出如下结论:
当我们发出POST值时,以下参数签名是无效的。
(1)原始缓存数据内容
(2)带有application/json content type的JSON字符串
(3)经过编码的表单变量
(4)QueryString变量
事实上,我们在POST发出请求中字符串内容时,此时字符串总是空,这样的结果对于Number、DateTime、byte[]皆是如此,在没有添加特性的情况下都是不会进行映射,除了复杂类型比如对象、数组等。由此我们不得不想到在Web APi中对于参数的绑定,参数绑定默认情况下是利用了某种算法进行映射,且都是基于媒体类型例如(content-type header) ,当我们POST一个字符串或者字节数组时,此时Web APi内部不知道如何去映射它,是将其映射到字节数组?是将其映射到字符串?还是将其映射到表单数据?不得而知,因此需要对此作出一些处理才行。请继续往下看。
为什么JSON字符串无效?
我们其实应该将其解释为原始字符串,而不是JSON字符串,令我们非常疑惑的是POST一个有application/json content type的JSON字符串将是无效的,像如下:
POST ......
Host: ......
Content-type: application/json; charset=utf-8
Content-Length: ...... "POST a JSON string"
此上是一个验证JSON的请求,但是结果是无法进行映射而失败。
添加【FromBody】特性到方法签名的参数中
我们可以通过参数绑定特性到方法签名上的参数中,这样就告诉Web APi这个内容的显式来源,【FromBody】抑或【FromUrl】特性强迫POST请求的中的内容会被进行映射。例如:
[HttpPost]
public string PostRaw([FromBody] string text)
{
return text;
}
这样之后就允许来自Body中的内容以JSON或者XML形式进行映射,以上是演示字符串,对于其他简单类型亦是如此,现在如果我们想POST,如下:
POST ......
Content-Type: application/json; charset=utf-8
Host: ......
Content-Length: ...... "POST a JSON string"
现在我们就行获得原始参数映射属性,因为输入的字符串是以JSON格式输入。从此知,用【FromBody】特性标记参数能够被映射,主要是对于要序列化的内容,例如:JSON或者XML。它要求数据以某种格式进行传输,【FromBody】当然也只能在单一POST表单变量中有效,但是它的限制是仅仅只能对于一个参数。
但是,假如我们想捕获整个原始内容利用【FromBody】将是无效的,也就是说,如果数据不会经过JSON或者XML编码的话,此时利用【FromBody】将毫无帮助。
捕获请求原始内容
如果我们不使用自定义扩展的参数绑定,我们还是有办法来捕获原始Http请求内容,但是此时无法将其原始捕获值赋到一个参数上,利用这个是非常的简单,代码如下:
[HttpPost]
public async Task<string> PostRaw()
{
string result = await Request.Content.ReadAsStringAsync();
return result;
}
ReadAsStringAsync 方法还有其他重载来捕获如byte[]或者Stream等原始内容,似乎非常简单。但是这样就解决问题了吗,如果是要捕获其他类型的呢?难道我们写重载方法吗?就我们所描述的问题,这根本不是解决方案,而是解决问题。千呼万唤始出来,最终解决方案出来了,请往下看。
创建自定义参数绑定
为了解决我们上述所描述捕获请求中的原始内容,我们不得的手动来实现的参数绑定,工作原理和【FromBody】实现方式类似,不过涉及Web APi中更多内容,感兴趣话可以参考我最后给出有关Web APi的整个生命周期去进行了解。为了解决这个问题,我们需要实现两点
(1)自定义参数绑定类
(2)自定义参数绑定特性来绑定参数
创建参数绑定类
首先,我们一个参数绑定特性类来获取请求中的内容并将其可以应用到任何控制器上的方法的参数上。 默认情况下是使用基于媒体类型的绑定来处理来自JSON或者XML的模型绑定或者原始数据绑定,我们通过使用【FromBody】、【FromUrl】或者【自定义参数绑定特性】来覆盖默认的参数绑定行为,当Web APi解析控制器上的方法签名时参数绑定会被调用。下面我们开始进行实现。
- 定义一个自定义参数绑定类,并继承于HttpParameterBinding
public class CustomParameterBinding : HttpParameterBinding
{
public CustomParameterBinding(HttpParameterDescriptor descriptor)
: base(descriptor)
{ } public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
var binding = actionContext
.ActionDescriptor
.ActionBinding; if (binding.ParameterBindings.Length > ||
actionContext.Request.Method == HttpMethod.Get)
return EmptyTask.Start();
}
......
}
- 若参数绑定同样只适用一个参数并且是非GET请求,若不满足,此时将执行一个空任务【EmptyTask】
public class EmptyTask
{
public static Task Start()
{
var taskSource = new TaskCompletionSource<AsyncVoid>();
taskSource.SetResult(default(AsyncVoid));
return taskSource.Task as Task;
} private struct AsyncVoid
{
}
}
- 当满足条件后,则进行参数类型判断并获取原始内容
if (type == typeof(string))
{
return actionContext.Request.Content
.ReadAsStringAsync()
.ContinueWith((task) =>
{
var stringResult = task.Result;
SetValue(actionContext, stringResult);
});
}
else if (type == typeof(byte[]))
{
return actionContext.Request.Content
.ReadAsByteArrayAsync()
.ContinueWith((task) =>
{
byte[] result = task.Result;
SetValue(actionContext, result);
});
}
- 综上,整个代码如下:
public class CustomParameterBinding : HttpParameterBinding
{
public CustomParameterBinding(HttpParameterDescriptor descriptor)
: base(descriptor)
{ } public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
var binding = actionContext
.ActionDescriptor
.ActionBinding; if (binding.ParameterBindings.Length > ||
actionContext.Request.Method == HttpMethod.Get)
return EmptyTask.Start(); var type = binding
.ParameterBindings[]
.Descriptor.ParameterType; if (type == typeof(string))
{
return actionContext.Request.Content
.ReadAsStringAsync()
.ContinueWith((task) =>
{
var stringResult = task.Result;
SetValue(actionContext, stringResult);
});
}
else if (type == typeof(byte[]))
{
return actionContext.Request.Content
.ReadAsByteArrayAsync()
.ContinueWith((task) =>
{
byte[] result = task.Result;
SetValue(actionContext, result);
});
} throw new InvalidOperationException("Only string and byte[] are supported for [CustomParameterBinding] parameters");
} public override bool WillReadBody
{
get
{
return true;
}
}
}
参数绑定方法 ExecuteBindingAsync() 方法用来处理参数的转换,通过上述Web APi提供给我们的ActionContext来根据参数类型决定参数是否是我们需要处理的参数,若检测到该请求为非GET请求并且参数只有一个那将进行接下来的处理,读取Body中的请求内容,最终调用SetValue()方法来设置其值到绑定参数上,否则将忽略绑定。稍微复杂一点的就是异步任务的操作逻辑,我们知道ExecuteBingdingAsync方法始终都要返回一个Task但是不能返回一个null或者不能获得一个服务器错误,所以当条件不满足时我们需要继续执行操作而不做任何其他事情,所以我们实现一个异步执行任务EmptyTask。
创建参数绑定特性
我们知道自定义实现了参数绑定,我们需要一个机制让Web APi知道一个参数需要这种绑定,所以我们需要将上述参数绑定类进行附加,此种自定义绑定作为默认绑定的话将作为最后一个绑定,但是这种情况下工作并不是很可靠,因为在执行到这里之前如果content type没有匹配到已经注册的媒体类型之一时,Web APi此时将会阻塞,因此一个明确的特性是可靠工作的唯一保证。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class CustomBodyAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
if (parameter == null)
throw new ArgumentException("Invalid parameter"); return new CustomParameterBinding(parameter);
}
}
上述CustomBodyAttribute特性继承自ParameterBindingAttribute,此唯一的目的是动态的确定此种绑定将被应用在使用了特性的参数上,这一切无非就是为了创建了上述参数绑定类的实例,并进行传递参数。
使用自定义参数绑定特性验证
上述操作已经全部完成,接下来就是实现,如下:
[HttpPost]
public string PostRawContent([CustomBody]string rawContent)
{ return rawContent;
}
单元测试
鉴于上述,我们利用单元测试来试试是否成功。我们利用Xunit来进行测试,代码如下:
public class UnitTest1
{
[Fact]
public async Task TestMethod1()
{
string url = "http://localhost:7114/api/product/PostRawContent"; string post = "Hello World"; var httpClient = new HttpClient();
var content = new StringContent(post);
var response = await httpClient.PostAsync(url, content); string result = await response.Content.ReadAsStringAsync(); Xunit.Assert.Equal(result, "\"" + post + "\"");
}
}
测试通过如下:
总结
【FromBody】只适用于接受经过JSON序列化的值,并且仅仅只能是一个参数,若我们想不经过JSON序列化而获得其原始值,那么用【FromBody】标记方法签名的参数将无效。
接受POST请求多个参数解决方案
利用模型绑定不再叙述
利用JSON Formatter
我们给出一个Person类,并在控制器上的方法中的参数中用此类变量来接受传递过来的值,如下:
public class User
{
public string Name { get; set; }
public int Age { get; set; } public string Gender { get; set; }
} public class ProductController : ApiController
{
[HttpPost]
public int PostUser(User user)
{ return user.Age;
}
}
前台进行传递参数:
var user = { Name: "xpy0928", Age: , Gender: "男" };
$("#btn").click(function () {
$.ajax({
type: "post",
url: "http://localhost:7114/api/product/PostUser/1",
dataType: "json",
data: JSON.stringify(user),
contentType: "application/json",
cache: false,
error: function (x, c, e) {
if (c == "error") {
$(this).val(c);
}
},
success: function (r) {
alert(r);
}
});
});
总结如下:
我们只需创建一个需要传递的参数对象,并利用JSON.stringfy将其序列化成JSON字符串即可
第三种解决方案
对于此种解决方案,我们需要首先来叙述下应用的场景,我们知道第一和第二种解决方案是类似的,这两种解决方案只不过在前台进行处理的方式不同而已,模型绑定总是有效主要是依靠一个单个的对象并将其映射到实体中,但是如果是如下的多个参数呢?
[HttpPost]
public int PostUser(User user,string userToken)
{}
这样的场景是很常见的,我们应该如何去求解呢?有如下几种解决办法
- 利用POST和QueryString联合解决,这就不再叙述
此种方式只能说暂时解决了问题,对于一个简单的参数用QueryString还可以,如果是多个复杂类型对象的话,这种方式将无效,因为QueryString不支持复杂类型映射,仅仅只对于简单类型才有效。
- 利用单个对象将两个参数进行包裹
我们简单的想象一下,如果如上述要接受这样的参数,我们可以将其作为一个对象来获取,就如同数学中的整体思想,将上述两个参数封装为一个对象来实现,一般来看的话,当我们发出POST请求最终肯定是要获得此请求的结果或者说是请求成功的状态,换言之,也就是我们输入应该包裹输入的多个参数,并且输出最终的结果值,也就是说利用Request和Response来获得其请求并作出响应。如下:
- 用户类依然不变
public class User { public string Name { get; set; }
public int Age { get; set; } public string Gender { get; set; }
}
- 包裹请求的两个参数
public class UserRequest
{ public User User { get; set; }
public string UserToken { get; set; }
}
- 最后响应结果
public class UserResponse
{
public string Result { get; set; } public int StatusCode { get; set; } public string ErrorMessage { get; set; }
}
- 控制器方法接受传入参数
[HttpPost]
public UserResponse PostUser(UserRequest userRequest)
{
var name = userRequest.User.Name;
var age = userRequest.User.Age;
var userToken = userRequest.UserToken; return new UserResponse()
{
StatusCode = 200,
Result = string.Format("name:{0},age:{1},userToken:{2}", name, age, userToken)
};
}
- 前台进行传递参数并将其序列化
var user = { Name: "xpy0928", Age: 12, Gender: "男" };
var userToken = "xpy09284356fd765fdf";
$("#btn").click(function () {
$.ajax({
type: "post",
url: "http://localhost:7114/api/product/PostUser/1",
dataType: "json",
data: JSON.stringify({ User: user, UserToken: userToken }),
contentType: "application/json",
cache: false,
error: function (x, c, e) {
if (c == "error") {
$(this).val(c);
}
},
success: function (r) {
alert(r);
}
});
});
接下来我们进行验证,是否接受成功
- 利用JObject解析多个属性(完美解决方案,你值得拥有)
上述似乎成功了解决了问题,但是我们不得不为方法签名创建用户接受和响应的对象,如果上述两个参数是频繁要用到,我们是不是就得每次都这样做,这样的话,我们就不能偷懒了,我们所说的懒,不是偷工减料而是有没有做成代码可复用的可能。我们想想,难道就不能将参数抽象成一个单个的对象并且为所有方法进行复用吗?好像很复杂的样子,确实,在JSON.NET未出世之前确实令人头疼,但是现在一切都将变得如此简单。
直接在Web APi上进行全自动包装是不可能的,但是有了JSON.NET代替JSON.Serializer我们就再也不用担心了,我们利用JObject来接受一个静态的JSON结果,并最终将JObject的子对象进行动态转换为强类型对象即可
- 控制器方法改造
[HttpPost]
public string PostUser(JObject jb)
{
dynamic json = jb; //获得动态对象
JObject userJson = json.User; //获取动态对象中子对象
string userToken = json.UserToken; var user = userJson.ToObject<User>(); //将其转换为强类型对象 return string.Format("name:{0},age:{1},userToken:{2}", user.Name, user.Age, userToken); }
- 前台调用不变
- 瞧瞧验证结果
总结
以上对于POST请求获取多个参数的方式可能不是最好的解决方法,将一堆参数串联起来供Web APi来调用,在理想情况下,Web APi是只接受单一的个参数,但是这并不意味着在任何场景下我们不需要应用上述方法,当我们需要传递几个对象到服务器上时有以上几种方式在不同场景下供我们选择并且是有效的。
说明
最近找工作中,所以博客暂时停止更新,Web APi原理还剩下参数绑定、模型绑定原理解析未更新,后续有时间再进行更新,下面给出Web APi整个生命周期的示意图,有想学习而不知从何学Web APi的原理的园友,可以借助此示意图进行参考学习。
示意图链接地址:Web APi生命周期示意图(ASP.NET Web APi Poster.PDF)
Web APi之捕获请求原始内容的实现方法以及接受POST请求多个参数多种解决方案(十四)的更多相关文章
- 解决.NET Web API生成的Help无Controller说明&服务端接收不到请求
今天在用.NET Web API写一个接口的时候遇到一个问题.在Controller中新加了一个方法,客户端就不能请求接口了,当时建WEB API项目是用的VS默认设置,在服务端打断点一直没有进去,而 ...
- 调用Web API将文件上传到服务器的方法(.Net Core)
最近遇到一个将Excel通过Web API存到服务器的问题,其中涉及到Excel的读取.调用API.Web API怎么进行接收. 一. Excel的读取.调用API Excel读取以及调用API的代 ...
- Web APi之认证
Web APi之认证(Authentication)两种实现方式后续[三](十五) 前言 之前一直在找工作中,过程也是令人着实的心塞,最后还是稳定了下来,博客也停止更新快一个月了,学如逆水行舟,不 ...
- ASP.NET Core Web APi获取原始请求内容
前言 我们讲过ASP.NET Core Web APi路由绑定,本节我们来讲讲如何获取客户端请求过来的内容. ASP.NET Core Web APi捕获Request.Body内容 [HttpPos ...
- Self Host模式下的ASP. NET Web API是如何进行请求的监听与处理的?
构成ASP.NET Web API核心框架的消息处理管道既不关心请求消息来源于何处,也不需要考虑响应消息归于何方.当我们采用Web Host模式将一个ASP.NET应用作为目标Web API的宿主时, ...
- ASP.NET Web Api 服务器端变了,客户端该如何修改请求(转载)
转载地址:http://www.cnblogs.com/fzrain/p/3558765.html 前言 一旦我们将API发布之后,消费者就会开始使用并和其他的一些数据混在一起.然而,当新的需求出现时 ...
- Web API 2 入门——使用ASP.NET Web API和Angular.js构建单页应用程序(SPA)(谷歌翻译)
在这篇文章中 概观 演习 概要 由网络营 下载网络营训练包 在传统的Web应用程序中,客户机(浏览器)通过请求页面启动与服务器的通信.然后,服务器处理请求,并将页面的HTML发送给客户端.在与页面的后 ...
- ASP.NET Web API - ASP.NET MVC 4 系列
Web API 项目是 Windows 通信接口(Windows Communication Foundation,WCF)团队及其用户激情下的产物,他们想与 HTTP 深度整合.WCF ...
- 初尝Web API《转》
HTTP 并不是只能用在网页中.它其实还是一个强大的平台,可以用来生成一些API,暴露服务和数据.HTTP很简单灵活,还非常普及.几乎所有你能想到的平台都有HTTP库,所以HTTP服务可以囊括很大范围 ...
随机推荐
- 谈谈rem
用rem已久但是对于它的理解似乎一直都有偏差,使用的时候多采用的是html的font-size:62.5%;然后按照1rem=10px这样来使用.所以我一直不明白,这个rem到底哪里是相对单位了,也不 ...
- webview使用技巧汇总
1.webview去除原网址的广告或者标题 js语句 document.documentElement.getElementsByClassName('这里写你要消除的空间的class里面的字符串') ...
- java分享第十九天(TestNg的IReporter接口的使用)
IReporter接口是干嘛的?就是让用户自定义报告的,很多人想要自定义报告,于是乎找各种插件,比如什么testng-xslt啊,reportng啊,各种配置,最后出来的结果,还不能定制化,但为什么 ...
- 客户端连接注册Ejabberd新用户
今天需要使用客户端注册新用户,结果发现注册失败,在管理后台添加新用户成功.编译安装ejabberd就没有管了,经过翻论坛的到解决方法 在ejabberd.yml中. access: trusted_n ...
- java反射学习之一反射机制概述
一.反射机制背景概述 1.反射(reflection)是java被视为动态语言的一个关键性质 2.反射机制指的是程序在运行时能获取任何类的内部所有信息 二.反射机制实现功能概述 1.只要给定类的全名, ...
- 解决overflow:hidden在安卓微信页面没有效果的办法
在做h5移动端时候,发现overflow: hidden;在安卓微信页面失效问题,经研究和实验,用第三种方法和第四种方法可以解决! 1.完全隐藏 在<boby>里加入scroll=&quo ...
- 享元模式 - Flyweight
Flyweight(享元模式) 定义 GOF:运用共享技术有效地支持大量细粒度的对象. GOF的定义比较专业化,通俗来说,当你有大量相似的实例时,你把其中相同的实例取出来共享. 例子 在你的游戏场景中 ...
- Android动画
[浅谈Android动画] 总共四种:Tween Animation变换动画.Frame Animation帧动画 Layout Animation布局动画.Property Animation 属性 ...
- UI控件(UITextField)
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UITextField* textField1 = ...
- Lesson 6 Percy Buttons
Text I have just moved to a house in Bridge Street. Yesterday a bagger knocked at my door. He asked ...