前言
之前對於用SelfHost來手動實現Web API的宿主模式,似乎不是太深入,所以本篇文章我們一起來討論關於利用HttpClient來訪問Web API上的資源來進行探討以及注意相關事項,希望此文對你也有收獲。
來自XML或Json Content的簡單參數
當Web API方法中接受如String、Datetime、Int等參數類型時,但是默認情況下這些方法不會接收來自XML或者JSON Body的參數,那么結果就是導致請求失敗。接下來我們來進行演示。
我們取名為Default的Web API控制器,方法以及路由注冊如下:
[HttpPost] public string GetString(string message) { return message; } config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{message}", defaults: new { message = RouteParameter.Optional } );
接下來我們用客戶端發出POST請求以及數據來請求Web API來並返回該數據,如下:
var client = new HttpClient(); var postUrl = "http://localhost:23133/api/default/GetString"; var task = client.PostAsJsonAsync<string>(postUrl,"Hello World").Result; //將參數進行序列化為JSON並發送到Request中的Body中
通過上述路由以及請求方式的配置理論上來說是能通過URI能訪問到資源,但是結果卻令你大跌眼鏡
由上可得知,參數Message的值Hello World已經傳遞過來,但是此時請求響應的資源上方法的參數並未接收到且返回404未找到該方法。
上面可能效果不是太明顯,我們利用Ajax傳遞時間來進行請求並返回該數據,方法如下:
public HttpResponseMessage GetDateTime(DateTime time) { return Request.CreateResponse<DateTime>(HttpStatusCode.OK, time); }
前台請求如下(路由配置就此略過):
$("#btn").click(function () { $.ajax({ type: "get", url: "http://localhost:23133/api/Default/GetDateTime", dataType: "json", contentType: "application/json; charset=utf-8", data: { "time": new Date("2015","09","22") }, cache: false, success: function (r) { console.log(r); } }); });
此時響應信息如下:
{"Message":"請求無效。","MessageDetail":"對於“WebApplication2.Controllers.DefaultController”中方法“System.Net.Http.HttpResponseMessage GetDateTime(System.DateTime)”的不可以為 null 的類型“System.DateTime”的參數“time”,參數字典包含一個 null 項。可選參數必須為引用類型、可以為 null 的類型或聲明為可選參數。"}
出錯原因是對於簡單的參數(如String、Int以及DateTime等)當進行請求時不會進行解析其屬性而會將其設置為null,所以傳遞過來的時間為null,而方法的時間參數又不能為空,所以會出現錯誤。對於這種情況我們使用特性[FromBody]來解決【注意】當在前台傳遞值為字符串或者數字時則是可以解析的,能夠成功傳遞到方法的參數時,而利用HttpClient進行傳遞時則會出現404。
鑒於上述,我們將其方法參數進行如下修飾即可成功
public HttpResponseMessage GetDateTime([FromBody] DateTime time)
建議
對於前台進行簡單參數進行傳遞以及和利用HttpClient來進行簡單參數的傳遞出現的情況不同,建議在其參數上加上特性 FromBody ,這樣兩者皆可避免。
補充
當然有FromBody也就對應的有FromURI,添加此特性就可獲得URL上該特性參數對應的值,URI如下:
var postUrl = "http://localhost:23133/api/default/GetString?message='xpy0928'";
被請求的資源如下:
[HttpPost] public string GetString([FromUri] string message) { return message; }
此時請求時該message對應的值就為xpy0928。
UrlEncoded表單字符串解析
我們在操作表單時肯定會有將表單數據作為字典進行傳遞的情況,下面我們就來演示這種情況。
如下為Web API需要被請求的方法
[HttpPost] public string GetString(string cnblogs) { return cnblogs; }
接下來我們利用客戶端來訪問上述該方法:
var formVars = new Dictionary<string, string>(); formVars.Add("cnblogs", "xpy0928"); var content = new FormUrlEncodedContent(formVars); var client = new HttpClient(); var postUrl = "http://localhost:23133/api/default/GetString"; var task = client.PostAsync(postUrl, content).Result; //參數未進行序列化
從上述可以看出理論上是可行的,我們通過URI將表單數據以字典形式進行傳遞去請求Web API,並獲取鍵cnblogs的值。結果會虐死你,如下:
因為這種對於cnblogs參數值映射到方法上的cnblogs參數上在Web API是行不通的,Web API不會進行映射 。
既然簡單的參數來接收不行下面我們嘗試利用對象來獲得該參數的值就像MVC中提交參數到實體上一樣。
首先給出一個類,並且該類中含有cnblogs屬性,如下:
public class CnblogsModel { public string Cnblogs { get; set; } }
將其方法及參數修改如下:
[HttpPost] public string GetString(CnblogsModel model) { return model.Cnblogs; }
接下來就是見證奇跡的時刻到了,給力啊AngelaBaby,皇天不負有心人,如下:
【注意】由上可知成功的關鍵是對象中的字段必須要和POST表單中的鍵必須相匹配,如果有多個鍵,那么對象中的字段也要與之對應。
上述的解決方案完全是可行的,但是讓人忍俊不禁的是這么做似乎有點太操蛋,接受表單中的參數還得建立對象並與之對應,所以得想更好的出路,既然是針對表單的,難道表單中對於接受這樣的字典就沒有對應的類來進行處理嗎,於是乎,開始找啊找,結果找到了一個 FormDataCollection (表面意思是表單數據集合)。貌似可行,來我們繼續將其代碼進行改寫,如下:
[HttpPost] public string GetString(FormDataCollection collection) { return collection.Get("cnblogs"); }
驗證通過,如下:
這樣就比第一個解決方案可行多了,只是對於這樣的集合類有點讓人想不明白,為什么不直接通過索引來獲取數據而非得用對於單個用Get或者對於多個字段用GetValues()方法
你是不是就以為這樣簡潔就是可行的呢?那你可就錯了,當我們在ASP.NET上運行Web API我們完全可以通過如下來進行獲取表單數據:
var request = HttpContext.Current.Request; var formKeys = request.Form.AllKeys;
建議
當獲取表單數據時建議用 HttpContext.Current.Request ,雖然它僅僅只能在ASP.NET上進行應用而無法在SelfHost上使用,但是對於一些復雜的場景下,比如說你需要獲得一些Web API上的Request沒有暴露的數據,此時用此種方法將是不可替代的,同時HttpContext無論是在ASP.NET還是MVC或者Web API中都是存在的。所以盡量避免使用FormDataCollection,即使看起來在路由上使用它更簡單。
補充(1)
感謝園友【敷衍不起】的疑問,剛開始以為挺簡單的,結果就是被打臉了,后經過研究對於復雜類型的數據的傳遞以及接收,若是具體類,利用FormDataCollection是行不通的,我能想到的解決方案是進行如下操作【期待你更好的解決方案】:
var p = new Person() { Name = "person", Student = new Student() { StuName = "xpy0928" } }; var client = new HttpClient(); var postUrl = new Uri("http://localhost:23133/api/default/GetString"); var task = client.PostAsJsonAsync(postUrl, p).Result; //利用PostAsJsonAsync方法來進行序列化類
在Web API上進行接收,如下:
[HttpPost] public string GetString(Person p) { return p.Name; }
至於用FormDataCollection接收不行的原因是 FormUrlEncodedContent 的參數鍵和值必須都是string類型,若類不是具體類直接添加鍵和值都是字符串則是可行的,但是這種情況似乎不太現實,所以對於具體類還是利用 PostAsJsonAsync 來進行解決。
補充(2)
感謝園友【YoMe和kennywangjin】的提醒,上述必須顯式通過路由來進行參數傳遞才會成功,如果通過Request中body來傳遞就會出現不解析的情況。
訪問QueryString
在命名空間System.Net.Http下有一擴展方法如下:
var requestUri = Request.RequestUri.ParseQueryString(); var paramValue = requestUri["param"];
在大多數場景下使用此方法是比較高效的,但是如果在URI中有許多參數,此時你不得不去顯式去解析這些參數。
總結
如上述,Web API對於相關參數的映射是比較簡單的,但是在一些不同的情況下,你不得不手動做一些工作來進行處理,同時我們也許意識到了Web API並沒有提供任何全局上下文對象,例如Request以及過濾中的Formatters等,都是基於Web API來提供特定的方法來進行操作即方法都是為Web API而量身定制,所以在看到Web API強大的同時也要看到其軟肋,這樣我們才能更好的對於不同場景下作出不同的處理。