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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

前言

我们知道在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天前
|
测试技术 API 项目管理
API测试方法
【10月更文挑战第18天】API测试方法
21 1
|
1天前
|
存储 安全 Go
Web安全基础:防范XSS与CSRF攻击的方法
【10月更文挑战第25天】Web安全是互联网应用开发中的重要环节。本文通过具体案例分析了跨站脚本攻击(XSS)和跨站请求伪造(CSRF)的原理及防范方法,包括服务器端数据过滤、使用Content Security Policy (CSP)、添加CSRF令牌等措施,帮助开发者构建更安全的Web应用。
10 3
|
1天前
|
SQL 存储 安全
Web 常见攻击方式及防御方法
【10月更文挑战第25天】Web 安全是一个复杂而重要的领域,攻击者不断寻找新的攻击方法,我们需要不断加强防御措施,提高安全意识,以保障 Web 应用的安全运行。通过采取多种防御手段的综合运用,我们可以有效地降低 Web 攻击的风险,保护用户的信息和财产安全。同时,随着技术的不断发展,我们也需要持续关注和研究新的安全威胁和防御方法,以应对不断变化的安全形势。
81 56
|
8天前
|
Java 大数据 API
别死脑筋,赶紧学起来!Java之Steam() API 常用方法使用,让开发简单起来!
分享Java Stream API的常用方法,让开发更简单。涵盖filter、map、sorted等操作,提高代码效率与可读性。关注公众号,了解更多技术内容。
|
7天前
|
缓存 负载均衡 API
抖音抖店API请求获取宝贝详情数据、原价、销量、主图等参数可支持高并发调用接入演示
这是一个使用Python编写的示例代码,用于从抖音抖店API获取商品详情,包括原价、销量和主图等信息。示例展示了如何构建请求、处理响应及提取所需数据。针对高并发场景,建议采用缓存、限流、负载均衡、异步处理及代码优化等策略,以提升性能和稳定性。
|
1天前
|
Java API PHP
阿里巴巴参数获取API
阿里巴巴的参数获取API流程包括:1. 注册并认证开发者账号;2. 创建应用,获取API密钥;3. 阅读API文档,了解请求参数和返回格式;4. 编写代码调用API,如使用Python请求商品详情;5. 注意API类型及其参数,遵守数据使用规则和法律法规。
|
7天前
|
缓存 监控 API
微服务架构下RESTful风格api实践中,我为何抛弃了路由参数 - 用简单设计来提速
本文探讨了 RESTful API 设计中的两种路径方案:动态路径和固定路径。动态路径通过路径参数实现资源的 CRUD 操作,而固定路径则通过查询参数和不同的 HTTP 方法实现相同功能。固定路径设计提高了安全性、路由匹配速度和 API 的可维护性,但也可能增加 URL 长度并降低表达灵活性。通过对比测试,固定路径在性能上表现更优,适合微服务架构下的 API 设计。
|
14天前
|
存储 前端开发 API
前端开发中,Web Storage的存储数据的方法localstorage和sessionStorage的使用及区别
前端开发中,Web Storage的存储数据的方法localstorage和sessionStorage的使用及区别
63 0
|
17天前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
75 3
|
23天前
|
设计模式 测试技术 持续交付
开发复杂Web应用程序
【10月更文挑战第3天】开发复杂Web应用程序
31 2