Web APi之捕获请求原始内容的实现方法以及接受POST请求多个参数多种解决方案(十四)

简介:

前言

我们知道在Web APi中捕获原始请求的内容是肯定是很容易的,但是这句话并不是完全正确,前面我们是不是讨论过,在Web APi中,如果对于字符串发出非Get请求我们则会出错,为何?因为Web APi对于简单的值不能很好的映射。之前我们谈论过请求内容注意事项问题,本节我们将更加深入的来讨论这个问题,我们会循序渐进进行探讨,并给出可行的解决方案,。细细品,定让你收货多多!

捕获请求原始内容实现方法

捕获复杂属性值

Web APi对于复杂属性值以JSON或者XML的形式成功发送到服务器,基于这点是非常容易而且简单的,如果我们想捕获一个对象,我们只需简单的创建一个控制并在其方法上有一个对象参数即可,因为Web APi会自动以解码JSON或者XML的处理形式到控制器上的方法参数对象中,如下:

1
2
3
4
[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请求的操作来捕获其值,这是很容易的,但是就如以上复杂类型它不会进行自动检测其类型进行映射,而且是不透明的。

我们可能会进行如下操作,并且认为结果会如我们所料,我们会认为获取其值并进行映射到方法上的参数中。

1
2
3
4
5
[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字符串将是无效的,像如下:

1
2
3
4
5
6
POST ......
Host: ......
Content-type: application/json; charset=utf-8
Content-Length: ......
 
"POST a JSON string"

此上是一个验证JSON的请求,但是结果是无法进行映射而失败。  

添加【FromBody】特性到方法签名的参数中 

我们可以通过参数绑定特性到方法签名上的参数中,这样就告诉Web APi这个内容的显式来源,【FromBody】抑或【FromUrl】特性强迫POST请求的中的内容会被进行映射。例如:

1
2
3
4
5
[HttpPost]
public  string  PostRaw([FromBody]  string  text)
{
     return  text;
}

这样之后就允许来自Body中的内容以JSON或者XML形式进行映射,以上是演示字符串,对于其他简单类型亦是如此,现在如果我们想POST,如下:

1
2
3
4
5
6
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请求内容,但是此时无法将其原始捕获值赋到一个参数上,利用这个是非常的简单,代码如下:

1
2
3
4
5
6
[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 > 1 ||
                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 > 1 ||
                actionContext.Request.Method == HttpMethod.Get)
                return EmptyTask.Start();

            var type = binding
                        .ParameterBindings[0]
                        .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,此唯一的目的是动态的确定此种绑定将被应用在使用了特性的参数上,这一切无非就是为了创建了上述参数绑定类的实例,并进行传递参数。

使用自定义参数绑定特性验证 

上述操作已经全部完成,接下来就是实现,如下:

1
2
3
4
5
6
[HttpPost]
public  string  PostRawContent([CustomBody] string  rawContent)
{
    
     return  rawContent;
}

单元测试  

鉴于上述,我们利用单元测试来试试是否成功。我们利用Xunit来进行测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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类,并在控制器上的方法中的参数中用此类变量来接受传递过来的值,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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: 12, 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字符串即可

第三种解决方案

对于此种解决方案,我们需要首先来叙述下应用的场景,我们知道第一和第二种解决方案是类似的,这两种解决方案只不过在前台进行处理的方式不同而已,模型绑定总是有效主要是依靠一个单个的对象并将其映射到实体中,但是如果是如下的多个参数呢?

1
2
3
[HttpPost]
public  int  PostUser(User user, string  userToken)
{}

这样的场景是很常见的,我们应该如何去求解呢?有如下几种解决办法

  • 利用POST和QueryString联合解决,这就不再叙述

此种方式只能说暂时解决了问题,对于一个简单的参数用QueryString还可以,如果是多个复杂类型对象的话,这种方式将无效,因为QueryString不支持复杂类型映射,仅仅只对于简单类型才有效。

  • 利用单个对象将两个参数进行包裹

我们简单的想象一下,如果如上述要接受这样的参数,我们可以将其作为一个对象来获取,就如同数学中的整体思想,将上述两个参数封装为一个对象来实现,一般来看的话,当我们发出POST请求最终肯定是要获得此请求的结果或者说是请求成功的状态,换言之,也就是我们输入应该包裹输入的多个参数,并且输出最终的结果值,也就是说利用Request和Response来获得其请求并作出响应。如下:

  • 用户类依然不变
1
2
3
4
5
6
7
public  class  User {
 
      public  string  Name {  get set ; }
      public  int  Age {  get set ; }
 
      public  string  Gender {  get set ; }
  }
  • 包裹请求的两个参数
1
2
3
4
5
6
public  class  UserRequest
{
 
     public  User User {  get set ; }
     public  string  UserToken {  get set ; }
}
  • 最后响应结果
1
2
3
4
5
6
7
8
public  class  UserResponse
{
     public  string  Result {  get set ; }
 
     public  int  StatusCode {  get set ; }
 
     public  string  ErrorMessage {  get set ; }
}
  • 控制器方法接受传入参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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)
     };
}
  • 前台进行传递参数并将其序列化 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)







本文转自Jeffcky博客园博客,原文链接:http://www.cnblogs.com/CreateMyself/p/4874273.html,如需转载请自行联系原作者

目录
相关文章
|
8天前
|
JSON 安全 API
Microsoft邮箱API发送邮件的方法和步骤
Aoksend详解如何使用Microsoft邮箱API发送邮件:1. 在Azure创建应用并获取访问权限;2. 设置API请求头,含Authorization和Content-Type;3. 构建JSON格式的邮件内容;4. 使用POST方法发送至API端点;5. 检查响应处理发送结果。遵循最佳实践,安全集成邮件功能。
|
11天前
|
搜索推荐 JavaScript 前端开发
Gmail邮箱API发送邮件的方法有什么
使用Gmail API发送邮件,需先获取API访问权限,包括在Google Cloud Platform上创建项目,启用Gmail API,生成API密钥或OAuth 2.0凭据。然后,用Python等编程语言设置API请求,指定邮件详情。发送简单邮件涉及创建Base64编码的消息体,而带附件的邮件需编码为multipart格式。可添加邮件头信息,并处理发送结果以确保成功。Gmail API使应用能集成自动化、个性化的邮件发送功能,提升效率和体验。
|
27天前
|
人工智能 Java API
Google Gemini API 接口调用方法
Google 最近发布的 Gemini 1.0 AI 模型通过其升级版,Gemini,标志着公司迄今为止最为强大和多功能的人工智能技术的突破。
|
1月前
|
JSON API 数据库
解释如何在 Python 中实现 Web 服务(RESTful API)。
解释如何在 Python 中实现 Web 服务(RESTful API)。
23 0
|
1月前
|
存储 开发框架 JSON
在 Python 中,如何处理 Web 请求和响应?
【2月更文挑战第26天】【2月更文挑战第90篇】在 Python 中,如何处理 Web 请求和响应?
|
1月前
|
人工智能 缓存 API
谷歌发布MediaPipe LLM Inference API,28亿参数模型本地跑
【2月更文挑战第24天】谷歌发布MediaPipe LLM Inference API,28亿参数模型本地跑
71 3
谷歌发布MediaPipe LLM Inference API,28亿参数模型本地跑
|
2月前
|
Prometheus 网络协议 JavaScript
api 网关 kong 数据库记录请求响应报文
Kong的tcp-log-with-body插件是一个高效的工具,它能够转发Kong处理的请求和响应。这个插件非常适用于需要详细记录API请求和响应信息的情景,尤其是在调试和排查问题时。
44 0
api 网关 kong 数据库记录请求响应报文
|
9天前
|
监控 安全 API
Office365邮箱API发送邮件有什么值得推荐的方法
使用Office365邮箱API发送邮件时,建议采用OAuth认证确保安全;利用RESTful API简化流程;借助官方客户端库加速集成;遵循最佳实践保障安全可靠;批量发送时使用异步方式提升效率;监控调试以解决问题;注意避免触发垃圾邮件过滤。AokSend提供高效发信服务,支持触发式接口和SMTP/API,独立IP确保高触达。
|
27天前
|
Java API Maven
email api java编辑方法?一文教你学会配置步骤
在Java开发中,Email API是简化邮件功能的关键工具。本文指导如何配置和使用Email API Java:首先,在项目中添加javax.mail-api和javax.mail依赖;接着,配置SMTP服务器和端口;然后,创建邮件,设定收件人、发件人、主题和正文;最后,使用Transport.send()发送邮件。借助Email API Java,可为应用添加高效邮件功能。
|
29天前
|
前端开发 API 数据处理
uni-app 封装api请求
uni-app 封装api请求
13 0