前一篇文章提到不靠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
{
    public class BlahController : ApiController
    {
        //宣告Model類別承接前端傳入資料
        public class Player
        {
            public int Id;
            public string Name;
            public DateTime RegDate;
            public int Score;
        }
        
        [HttpPost]
        public string 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"
                };
                throw new 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
    {
        static void 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 As String)
    Dim xhr
    Set xhr = CreateObject("MSXML2.ServerXMLHTTP")
    Dim url As String
    url = "httq://localhost:32767/blah/insert"
    xhr.Open "POST", url, False
    xhr.SetRequestHeader "Content-Type", "application/json"
    On Error GoTo HttpError:
    xhr.Send body
    MsgBox xhr.responseText
    Exit Sub
HttpError:
    MsgBox "Error: " & Err.Description
End Sub
 
Sub Test()
    SendApiRequest "BAD THING"
    SendApiRequest "{ ""Id"":1,""Name"":""Jeffrey"", " & _
                   """RegDate"":""2012-12-21"", ""Score"":32767 }"
End Sub

Comments

# by wpf

Thank you a lot.

# by wpf

你好,Client 端 string url = "httq://localhost:32767/blah/Insert"; 有打錯,應該是 http。

# by Jeffrey

to wpf, 寫成 httq 是為了避免無效網址被系統當成有效連結。

Post a comment