天天看点

c#之webapi

c#之webapi
 

 不管是因为什么原因,结果是在新出的MVC中,增加了WebAPI,用于提供REST风格的WebService,个人比较喜欢REST风格的WebService,感觉比SOAP要轻量级一些,而且对客户端的要求也更少,更符合网络数据传输的一般模式,客户端完全摆脱了代理和管道来直接和WebService进行交互,具体的区别可以参见Web 服务编程,REST 与 SOAP


(一)环境准备
 

 本机的环境是XP+VS2010,需要安装VS2010 SP1升级包,MVC4升级包,Vs2010安装SP1后会影响SQLServer2008的自动提示功能,需要在安装补丁或插件,安装成功后可以新建 MVC WebAPI 项目


(二)概览
 

 新生成的WebAPI项目和典型的MVC项目一样,包含主要的Models,Views,Controllers等文件夹和Global.asax文件


 Views对于WebAPI来说没有太大的用途,Models中的Model主要用于保存Service和Client交互的对象,这些对象默认情况下会被转换为Json格式的数据进行传输,Controllers中的Controller对应于WebService来说是一个Resource,用于提供服务。和普通的MVC一样,Global.asax用于配置路由规则



(三)Models
 

 和WCF中的数据契约形成鲜明对比的是,MVC WebAPI中的Model就是简单的POCO,没有任何别的东西,如,你可以创建如下的Model


     public class TestUseMode

     {

         public string ModeKey{get;set;}

         public string ModeValue { get; set; }

         

     }

 注意:Model必须提供public的属性,用于json或xml反序列化时的赋值



(四)Controllers
 

 WebAPI中的Controllers和普通MVC的Controllers类似,不过不再继承于Controller,而改为继承API的ApiController,一个Controller可以包含多个Action,这些Action响应请求的方法与Global中配置的路由规则有关,在后面结束Global时统一说明



(五)Global
 默认情况下,模板自带了两个路由规则,分别对应于WebAPI和普通MVC的Web请求,默认的WebAPI路由规则如下


      routes.MapHttpRoute(

          name: "DefaultApi",

          routeTemplate: "api/{controller}/{id}",

          defaults: new { id = RouteParameter.Optional }

      );

 可以看到,默认路由使用的固定的api作为Uri的先导,按照微软官方的说法,用于区分普通Web请求和WebService的请求路径:


 Note: The reason for using "api" in the route is to avoid collisions with ASP.NET MVC routing. That way, you can have "/contacts" go to an MVC controller, and "/api/contacts" go to a Web API controller. Of course, if you don't like this convention, you can change the default route table.


 可以看到,默认的路由规则只指向了Controller,没有指向具体的Action,因为默认情况下,对于Controller中的Action的匹配是和Action的方法名相关联的:


 具体来说,如果使用上面的路由规则,对应下面的Controller:


     public class TestController : ApiController

     {

         public static List<TestUseMode> allModeList = new List<TestUseMode>();



         public IEnumerable<TestUseMode> GetAll()

         {

             return allModeList;

         }


         public IEnumerable<TestUseMode> GetOne(string key)

         {

             return allModeList.FindAll((mode) => { if (mode.ModeKey.Equals(key)) return true; return false; });

         }


         public bool PostNew(TestUseMode mode)

         {

             allModeList.Add(mode);

             return true;

         }


         public int Delete(string key)

         {

             return allModeList.RemoveAll((mode) => { if (mode.ModeKey == key) return true; return false; });

         }


         public int DeleteAll()

         {

             return allModeList.RemoveAll((mode) => { return true; });

         }



         public int PutOne(string key, string value)

         {

             List<TestUseMode> upDataList = allModeList.FindAll((mode) => { if (mode.ModeKey == key) return true; return false; });

             foreach(var mode in upDataList)

             {

                 mode.ModeValue = value;

             }

             return upDataList.Count;

         }

     }


 则,会有下面的对应关系:

 url
 http  method                
对应的action名

 /api/Test
 GET
  GetAll

 /api/Test/1
 GET                              
GetOne

 /api/Test
 POST                           
PostNew

 /api/Test/1
 DELETE                      
Delete

 /api/Test
 DELETE                      
DeleteAll

 /api/Test
 PUT
  PutOne


 简单使用JS调用上面提供的数据接口


          function getAll() {

              $.ajax({

                  url: "api/Test/",

                  type: 'GET',

                  success: function (data) {

                      document.getElementById("modes").innerHTML = "";

                      $.each(data, function (key, val) {

                          var str = val.ModeKey + ': ' + val.ModeValue;

                          $('<li/>', { html: str }).appendTo($('#modes'));

                      });

                  }

              }).fail(

              function (xhr, textStatus, err) {

                  alert('Error: ' + err);

              });

          }

  

          function add() {

  

              $.ajax({

                  url: "api/Test/",

                  type: "POST",

                  dataType: "json",

                  data: { "ModeKey": document.getElementById("txtKey").value, "ModeValue": document.getElementById("txtValue").value },

                  success: function (data) {

                      getAll();

                  }

              }).fail(

              function (xhr, textStatus, err) {

                  alert('Error: ' + err);

              });

  

          }

  

          function find() {

              

              $.ajax({

                  url: "api/Test/" + document.getElementById("txtFindKey").value,

                  type: 'GET',

                  success: function (data) {

                      document.getElementById("modes").innerHTML = "";

                      $.each(data, function (key, val) {

                          var str = val.ModeKey + ': ' + val.ModeValue;

                          $('<li/>', { html: str }).appendTo($('#modes'));

                      });

                  }

              }).fail(

              function (xhr, textStatus, err) {

                  alert('Error: ' + err);

              });

          }

  

          function removeAll() {

              $.ajax({

                  url: "api/Test/",

                  type: 'DELETE',

                  success: function (data) {

                      document.getElementById("modes").innerHTML = "";

                      getAll();

                  }

              }).fail(

              function (xhr, textStatus, err) {

                  alert('Error: ' + err);

              });

          }

  

          function remove() {

              $.ajax({

                  url: "api/Test/"+document.getElementById("txtRemoveKey").value,

                  type: 'DELETE',

                  success: function (data) {

                      document.getElementById("modes").innerHTML = "";

                      getAll();

                  }

              }).fail(

              function (xhr, textStatus, err) {

                  alert('Error: ' + err);

              });

          }

  

          function update() {

              $.ajax({

                  url: "api/Test/",

                  type: 'PUT',

                  dataType: "json",

                  data: { "key": document.getElementById("txtUpdateKey").value, "value": document.getElementById("txtUpdateValue").value },

                  success: function (data) {

                      document.getElementById("modes").innerHTML = "";

                      getAll();

                  }

              }).fail(

              function (xhr, textStatus, err) {

                  alert('Error: ' + err);

              });

          }


 这样就实现了最基本的CRUD操作。


(六)路由规则扩展
 和普通的MVC一样,MVC WebAPI支持自定义的路由规则,如:在上面的操作中,路由规则使用

 "api/{controller}/{id}"

 则限定了使用GET方式利用URL来传值时,controller后面的接收参数名为id,但是在Controller中,GetOne方法的接收参数名为key,是不会被匹配的,这时只需要新增一个新的路由规则,或修改原先的路由规则为:


 "api/{controller}/{key}"

 当然,可以对路由进行更深的扩展,如:扩展成和普通MVC一样的路由:

 "api/{controller}/{action}/{id}"

 这样,就要求同时使用Action和HTTP方法进行匹配

 当然,根据微软的说法,这种使用是不被推荐的,因为这不符合大家对WebService的一般认知:

 For a RESTful API, you should avoid using verbs in the URIs, because a URI should identify a resource, not an action.


(七)使用Attribute声明HTTP方法
 

 有没有感觉默认的使用方法名来匹配HTTP Method的做法很傻??或者我有一些方法是自己用的,不想暴露出来,又该怎么办?还是使用attribute做这些工作感觉优雅一些,比如,上面的Action我可以更改为:


         [HttpGet]

         public IEnumerable<TestUseMode> FindAll()


         [HttpGet]

         public IEnumerable<TestUseMode> FindByKey(string key)


         [HttpPost]

         public bool Add(TestUseMode mode)


         [HttpDelete]

         public int RemoveByKey(string key)


         [HttpDelete]

         public int RemoveAll()


         [HttpPut]

         public int UpdateByKey(string key, string value)


        [NonAction]  

        public string GetPrivateData()


 当然,我只列出了方法名,而不是这些方法真的没有方法体...方法体是不变的,NoAction表示这个方法是不接收请求的,即使以GET开头。

 如果感觉常规的GET,POST,DELETE,PUT不够用,还可以使用AcceptVerbs的方式来声明HTTP方法,如:


         [AcceptVerbs("MKCOL", "HEAD")]

         public int UpdateByKey(string key, string value)

         {

             List<TestUseMode> upDataList = allModeList.FindAll((mode) => { if (mode.ModeKey == key) return true; return false; });

             foreach(var mode in upDataList)

             {

                 mode.ModeValue = value;

             }

             return upDataList.Count;

         }

 Web API工作方式
 上面习了建立基本的WebAPI应用,立刻就有人想到了一些问题:

 1.客户端和WebService之间文件传输

 2.客户端或者服务端的安全控制

 要解决这些问题,要了解一下WebAPI的基本工作方式。


 (一)WebAPI中工作的Class


 在MVC中大家都知道,获取Request和Response使用HttpRequest和HttpResponse两个类,在WebAPI中使用另外两个类:

 HttpRequestMessage 和HttpResponseMessage,分别用于封装Requset和Response。除了这两个类之外,还有一个常见的抽象类:HttpMessageHandler,用于过滤和加工HttpRequestMessage和HttpResponseMessage


 (二)解决第一个问题


 其实第一个问题之所以被提出来应该是和客户端有关,如果客户端的请求是我们手写提交的,比如使用HttpClient封装的请求,则要传递文件之前,我们一 般会进行一次序列化,转化为二进制数组之类的,在网络上传输。这样的话,在Controller中的Action参数里,我们只需要接收这个二进制数组类型的对象就可以了。但是如果客户端是Web Form呢,比如我们提交一个Form到指定的Controller的Action中,这个Action要接收什么类型的参数呢?或者我们问另外一个问题,如果我将Web Form提交到一个WebAPI的Action中 ,我要怎么去取出这个表单中的数据呢?其实我们应该想到:我们的Action设置的参数之所以能够被赋值,是因为WebAPI的架构中在调用Action时将HTTP请求中的数据解析出来分别赋 值给Action中的参数,如果真是这样的话,我们只需要在Action中获取到HTTP请求,然后直接获取请求里面的数据,就能解决上面的问题。这种想法是正确的,只不过,此时的HTTP请求已经不是最原始的HTTPRequest,而是已经被转化成了HttpRequestMessage,在Action中,我们可以直接调用base.Requet得到这个 HttpRequestMessage实例,通过这个实例我们就可以随心所欲的取出HTTP请求中想要的数据。


 2.1从RequestMessage中获取普通表单数据


 这里的普通表单是指不包含File的表单,也就是说表单的enctype值不是multipart/form-data,这时,表单的数据默认情况下是以Json来传递的

 如下页面


 <form name="form" action="~/api/FormSubmit?key=11234" method="post">

     <input type="text" name="key" id="txtKey" />

     <br />


     <input type="text" name="value" id="txtValue" />

     <br />

     

     <input type="submit" id="btnSubmit" value="Submit" />

      

 </form>


 捕获到的请求为

 key=123&value=123


 提交到对应的Action为:

         [HttpPost]

         public async void submitForm()

         {

             StringBuilder sb = new StringBuilder();

             HttpContent content = Request.Content;

             JsonObject jsonValue = await content.ReadAsOrDefaultAsync<JsonObject>();

             foreach (var x in jsonValue)

             {

                 sb.Append(x.Key);

                 string va ;

                 if (x.Value.TryReadAs<string>(out va))

                 {

                     sb.Append(va);

                 }

             }

         }


 这样最后可以得到 Json的值:{"key":"123","value":"123"}  sb处理后的值为:key123value123

  

 注:在该action中使用到了关键字async和await,这些在4.5中新提出的关键字主要是用于进行多线程取值的,在MVCAPI的设计中,大部分的方法都被设计成类似于下面的方法


 public static Task<T> ReadAsOrDefaultAsync<T>(this HttpContent content);

 返回值是一个Task,这种返回新线程的方法虽然可以提高系统的响应能力,但是多线程取值会给编码带来不便,所以新出的关键字await用于阻塞当前线程并 获取目标线程的返回值,在方法体中使用await关键字后要求将方法声明为async用来表示该方法是异步的,并且返回值必须为void或者将返回者封装 在一个Task中,当然,如果你不喜欢这种写法,上面的action也可以写为:


             Task readTask = content.ReadAsOrDefaultAsync<JsonObject>().ContinueWith((task) => { jsonValue = task.Result; });

             readTask.Wait();


 2.2从RequestMessage中获取multipart表单数据

 将view页面改写为


 <form name="form" action="~/api/FormSubmit?key=11234" method="post" enctype="multipart/form-data" >

     <input type="text" name="key" id="txtKey" />

     <br />

     <input type="text" name="value" id="txtValue" />

     <br />

     <input type="file" name="file" id="upFile" />

     <br />

     <input type="submit" id="btnSubmit" value="Submit" />

 </form>


 此时捕获到得请求是 略


 这里的文件内容被捕获软件解析成字符串,当然如果我上传的是其他的非文本格式的文件,文件会被转化为二进制数组这时如果我们不更改action,而直接调用,会发生错误,原因很明显,这个HTTP的报文内容是无法被转换为JSON的,这时我们需要将表单的报文解析成另外一种格式


                 IEnumerable<HttpContent> bodyparts = await content.ReadAsMultipartAsync();

                 foreach (var bodypart in bodyparts)

                 {

                     string name;

                     name = bodypart.Headers.ContentDisposition.Name;

                     sb.Append(name + ":");

                     if (bodypart.Headers.Contains("filename"))

                     {

                         Stream stream = await bodypart.ReadAsStreamAsync();

                         StreamReader reader = new StreamReader(stream);

                         sb.Append(reader.ReadToEnd());

                         sb.Append("----");

                     }

                     else

                     {

                         string val = await bodypart.ReadAsStringAsync();

                         sb.Append(val);

                         sb.Append("----");

                     }

                 }


 得到的处理后的sb值为:


 {"key":123----"value":123----"file":******{文件的内容}*****----}

 整合后的Action为


         [HttpPost]

         public async void submitForm()

         {

             StringBuilder sb = new StringBuilder();

             HttpContent content = Request.Content;

             if (content.IsMimeMultipartContent())

             {

                 IEnumerable<HttpContent> bodyparts = await content.ReadAsMultipartAsync();

                 foreach (var bodypart in bodyparts)

                 {

                     string name;

                     name = bodypart.Headers.ContentDisposition.Name;

                     sb.Append(name + ":");

                     if (bodypart.Headers.Contains("filename"))

                     {

                         Stream stream = await bodypart.ReadAsStreamAsync();

                         StreamReader reader = new StreamReader(stream);

                         sb.Append(reader.ReadToEnd());

                         sb.Append("----");

                     }

                     else

                     {

                         string val = await bodypart.ReadAsStringAsync();

                         sb.Append(val);

                         sb.Append("----");

                     }

                 }

             }

             else

             {

                 JsonObject jsonValue = await content.ReadAsOrDefaultAsync<JsonObject>();

                 foreach (var x in jsonValue)

                 {

                     sb.Append(x.Key);

                     string va;

                     if (x.Value.TryReadAs<string>(out va))

                     {

                         sb.Append(va);

                     }

                 }

             }

         }


 (三)WebAPI工作方式



 要想解决第二个问题就没这么容易了,我们需要更深入的理解WebAPI的工作方式。

 其实对于WebAPI来说,它最初被设计为和WCF一样的:客户端、服务端两套结构,我们到现在之所以还没有提到客户端,是因为我们的请求别的方式来封装成HTTP请求或接收HTTP相应的,比如AJAX和Form表单提交。


 在这里先给出一个服务端的响应工作流,让大家有个大体上的认识 图略


 大家可以看到,HTTP的请求最先是被传递到HOST中的,如果WebAPI是被寄宿在IIS上的,这个HOST就是IIS上,HOST是没有能力也没有必要进行请求的处理的,请求通过HOST被转发给了HttPServer此时已经进入WebAPI的处理加工范围,HttpServer是System.Net.HTTP中的一个类,通过HttpServer,请求被封装成了WebAPI中的请求承载类:HttpRequestMessage,这个封装后的请求可以经过一系列自定义的Handler来处理,这些handler串联成一个pipeline,最后请求会被传递给HttpControlDispather,这个类通过对路由表的检索来确定请求将被转发到的具体的 Controller中的Action。


 Client端的处理与服务端类似,直接上图:略


 其实根据微软的说法,他们本身就被设计成类似但是可以独立运行的结构


 ASP.NET Web API has a pipeline for processing HTTP messages on both the client and server. The client and server sides are designed to be symmetrical but independent; you can use each half by itself. Both sides are built on some common objects:


 HttpRequestMessage represents the HTTP request.

 HttpResponseMessage represents the HTTP response.

 HttpMessageHandler objects process the request and response.

 直接看图 略 在客户端,Handlers pipeline最终是被传递到HttpClientHandler上的,由他负责HttpRequestMessage到HTTP请求的转换。


 这里只说明一下Request,Response与其类似。


 (四)解决第二个问题


 由 此我们早就可以看出,想要解决第二个问题,可以直接在Handler PipeLine中进行,这种AOP风格的过滤器(拦截器)在REST的Webservice的安全验证中应用很广,一般大家比较乐于在HTTP头或者在 HTTP请求的URL中加上身份验证字段进行身份验证,下面举一个在Http头中添加身份验证信息的小例子


 4.1客户端

 客户端的customhandler用于将身份验证信息添加入报头


     class RequestUpHandler : DelegatingHandler

     {

         protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)

         {

             request.Headers.Add("key", "11234");

             return base.SendAsync(request, cancellationToken);

         }

     }

 注:

 1.customhandler继承自DelegatingHandler类,上面已经说过,WebAPI的客户端和服务端被设计为相互对应的两套结构,所以不论是在客户端还是服务端,customhandler都是继承自DelegatingHandler类

 2.DelegatingHandler的sendAsync方法便是处理请求和接受请求时会被调用的方法,该方法返回值是HttPResponseMessage,接收的值为HttpRequestMessage,符合我们的一般认知

 3.方法的最后,调用base.SendAsync是将Request继续向该pipeline的其他customHandler传递,并获取其返回值。由于该方法不包含Response的处理逻辑,只需直接将上一个CustomHandler的返回值直接返回

 客户端主程序



         static void Main(string[] args)

         {

             HttpClient client = new HttpClient(new RequestUpHandler() { InnerHandler = new HttpClientHandler() });

             HttpResponseMessage response = client.GetAsync("http://localhost:60023/api/FormSubmit").Result;

             response.Content.ReadAsAsync<string>().ContinueWith((str) => { Console.WriteLine(str.Result); });

             Console.Read();

         }

 客户端的主程序创建了一个HttpClient,HttpClient可以接受一个参数,该参数就是CustomHandler,此处我们嵌入了我们定义的 RequestUpHandler,用于对Request报头进行嵌入身份验证码的处理,CustomHandler通过InnerHandler属性嵌 入其内置的下一个CustomHandler,此处,由于没有下一个CustomerHandler,我们直接嵌入HttpClientHandler用 于将HttpRequestMessage转化为HTTP 请求、将HTTP响应转化为HttpResponseMessage


 4.2服务端

 服务端的customHandler用于解析HTTP报头中的身份认证码


         protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)

         {

             int matchHeaderCount = request.Headers.Count((item) =>

             {

                 if ("key".Equals(item.Key))

                 {

                     foreach (var str in item.Value)

                     {

                         if ("11234".Equals(str))

                         {

                             return true;

                         }

                     }

                 }

                 return false;

             });

             if (matchHeaderCount>0)

             {

                 return base.SendAsync(request, cancellationToken);

             }

             return Task.Factory.StartNew<HttpResponseMessage>(() => { return new HttpResponseMessage(HttpStatusCode.Forbidden); });

         }


 注:代码的处理逻辑很简单:如果身份验证码匹配成功,则通过base.SendAsync继续将请求向下传递,否则返回直接中断请求的传递,直接返回一个响应码为403的响应,指示没有权限。

 注意由于SendAsync的返回值需要封装在Task之中,所以需要使用Task.Factory.StartNew将返回值包含在Task中


 将customHandler注入到HOST中

 本例中WebAPI HOST在IIS上,所以我们只需将我们定义的CustomHandler在Application_Start中定义即可



         protected void Application_Start()

         {

             //省略其他逻辑代码



             GlobalConfiguration.Configuration.MessageHandlers.Add(new HttpUrlHandler());

         }

 由于WebAPI Host在IIS上,所以HttpServer和HttpControllerDispatcher不用我们手工处理


 在加上上面的处理后,如果没有身份验证码的请求,会得到如下的响应


 HTTP错误 403.0 - Forbidden

 *****      

继续阅读