前一篇文章提到不靠IIS在Console/WinForm/WPF程式裡也可以執行ASP.NET Web API,接著我們更深入一點,談談Client端如何傳遞資料給ASP.NET Web API。
在ASP.NET Web API的傳統應用,Client端多是網頁,故常見範例是透過HTML Form、JavaScript、AJAX傳送參數資料給Web API;而在Self-Hosted ASP.NET Web API情境,由於Web API常被用於系統整合,呼叫端五花八門,.NET程式、VBScript、Excel VBA、Java... 都有可能,所幸Web API建構在HTTP協定之上,不管平台為何,都不難找到可用的HTTP Client元件或函式庫。
本文將示範我自己常用的兩種平台: .NET Client及Excel VBA。
首先,我們改寫前文範例,加上接受前端傳入Player物件新增資料的Insert() Action。由於ASP.NET MVC的ModelBinder已具備將JSON字串轉為Model物件的能力,我們也沒什麼好客氣的,直接宣告Player物件當成Insert()的輸入參數,JSON字串轉物件的工作就丟給ASP.NET MVC底層傷腦筋。
BlahController.cs改寫如下,程式碼很單純,唯一的小手腳是要捕捉例外,產生自訂錯誤訊息的HttpResponseMessage,再以其為基礎拋出HttpResponseException,理由容後說明。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http;
using Newtonsoft.Json;
namespace SelfHostWebApi
{
publicclass BlahController : ApiController
{
//宣告Model類別承接前端傳入資料
publicclass Player
{
publicint Id;
publicstring Name;
public DateTime RegDate;
publicint Score;
}
[HttpPost]
publicstring Insert(Player player)
{
try
{
//輸出資料,驗證已正確收到
Console.WriteLine("Id: {0:0000} Name: {1}", player.Id, player.Name);
Console.WriteLine("RegDate: {0:yyyy-MM-dd} Score: {1:N0}",
player.RegDate, player.Score);
return"Player [" + player.Id + "] Received";
}
catch (Exception ex)
{
//發生錯誤時,傳回HTTP 500及錯誤訊息
var resp = new HttpResponseMessage()
{
StatusCode = HttpStatusCode.InternalServerError,
Content = new StringContent(ex.Message),
ReasonPhrase = "Web API Error"
};
thrownew HttpResponseException(resp);
}
}
}
}
呼叫端的寫法很簡單,WebClient.UploadString(url, jsonString)會以jsonString為內容丟出HTTP POST請求,但有個關鍵: 必須設定ContentType為application/json,告知ModelBinder我們所POST的內容是JSON字串,ModelBinder才能正確地反序列化成Player類別。測試程式另外亂傳空字串及非JSON字串,以測試輸入錯誤時Web API的反應。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Newtonsoft.Json;
namespace ApiTest
{
class Program
{
staticvoid Main(string[] args)
{
WebClient wc = new WebClient();
string url = "httq://localhost:32767/blah/Insert";
//由WebException中取出Response內容
Action<string> postJson = (json) =>
{
try
{
//重要: 需宣告application/json,才可正確Bind到Model
wc.Headers.Add(HttpRequestHeader.ContentType,
"application/json");
var test = wc.UploadString(url, json);
Console.WriteLine("Succ: " +test);
}
catch (WebException ex)
{
StreamReader sr = new StreamReader(
ex.Response.GetResponseStream());
Console.WriteLine("Error: " + sr.ReadToEnd());
}
};
//故意傳入無效資料進行測試
postJson(string.Empty);
postJson("BAD THING");
//利用匿名型別+Json.NET傳入Web API所需的Json格式
var player = new
{
Id = 1,
Name = "Jeffrey",
RegDate = DateTime.Today,
Score = 32767
};
postJson(JsonConvert.SerializeObject(player));
Console.ReadLine();
}
}
}
測試結果如下:
Error: Object reference not set to an instance of an object. Error: Object reference not set to an instance of an object. Succ: "Player [1] Received"
當傳入空字串及非JSON字串,UpdateString()會發生WebException,而透過WebException.Response.GetResponseStream()可讀取Insert()方法在捕捉例外時透過HttpResponseMessage傳回的訊息內容。如果我們不捕捉例外,任由MVC內建機制處理,則Client會收到Exception經JSON序列化後的結果(如下所示),資訊較詳細但需反序列化才能讀取。相形之下,拋回HttpResponseException可以精準地控制傳回的錯誤訊息及提示,更容易符合專案客製需求。
Error: {"Message":"An error has occurred.","ExceptionMessage":"Object reference
not set to an instance of an object.","ExceptionType":"System.NullReferenceExcep
tion","StackTrace":" at SelfHostWebApi.BlahController.InsertByBinding(Player p
layer) in x:\\Temp\\Lab0603\\SelfHostWebApi\\SelfHostWebApi\\BlahController.cs:l
ine 34\r\n at lambda_method(Closure , Object , Object[] )\r\n at System.Web.
Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass1
3.<GetExecutor>b__c(Object instance, Object[] methodParameters)\r\n at System.
Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object
instance, Object[] arguments)\r\n at System.Threading.Tasks.TaskHelpers.RunSy
nchronously[TResult](Func`1 func, CancellationToken cancellationToken)"}
最後補上VBA寫法:
Sub SendApiRequest(body AsString)
Dim xhr
Set xhr = CreateObject("MSXML2.ServerXMLHTTP")
Dim url AsString
url = "httq://localhost:32767/blah/insert"
xhr.Open "POST", url, False
xhr.SetRequestHeader "Content-Type", "application/json"
OnErrorGoTo HttpError:
xhr.Send body
MsgBox xhr.responseText
ExitSub
HttpError:
MsgBox "Error: "& Err.Description
EndSub
Sub Test()
SendApiRequest "BAD THING"
SendApiRequest "{ ""Id"":1,""Name"":""Jeffrey"", "& _
"""RegDate"":""2012-12-21"", ""Score"":32767 }"
End Sub