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
*****