天天看點

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

 *****      

繼續閱讀