Uncategorized

ASP.NET Web Api で Custom MIME (ics ファイル) を処理

(2012/02 : サンプル コードを、WCF Web Api から ASP.NET Web API に変更)

(2012/06 : サンプル コードを、ASP.NET Web API RC 版にあわせて変更)

環境 : Visual Studio 2010, ASP.NET Web API RC (ASP.NET MVC 4 RC)

REST サービス / Web Api の実践

ここでは、応用的なテーマをとりあげます。基本的な構築手順については、「Getting Started with ASP.NET Web Api」 (ASP.NET の場合)、または「REST サービスの作成」 (WCF の場合) を参照してください。

こんにちは。

今回は、RESTful な Web Api で、さまざまな MIME タイプのデータを扱う方法について説明します。

.NET による Web Api 構築では、既定で、Plain XML、JSON などのフォーマットを扱えるようになっており、プログラム側で扱うオブジェクトも、これらの一般的なフォーマットに自然な形で (プログラマーがシリアライズ方法などを意識せずに) シリアライズできます。ASP.NET Web API WCF Web API を使用すると、実は、こうした一般的なフォーマット以外の独自のフォーマット (MIME Type) を扱えるように拡張できます。例えば、オブジェクトをイメージとして返したり、業界固有のフォーマット (MIME Type) なども扱うことができます。

補足 : なお、.NET Framework 4 の WCF REST サービス (WCF Web API を使用しない REST サービス) でも、Stream 型を使うことで、任意のデータ型を扱うことができます。(ただし、下記で述べるようなフォーマット拡張用のフレームワークはありません。)

 

プロジェクトの準備

準備として、ASP.NET Web API を使用した基本的な REST サービスを構築し、その後で、このサービスを拡張して行きましょう。

まず、Visual Studio を開き、ASP.NET MVC 4 Web Application のプロジェクトを新規作成します。(Web Api のプロジェクトを作成します。)

プロジェクトに、コントローラーを追加します。(今回、この名前を SchedulerController.cs とします。) そして、SchedulerController.cs に、以下の通りコードを記述します。

. . .using System.Net;using System.Net.Http;using System.Web.Http;. . .public class SchedulerController : ApiController{  // GET /api/scheduler/5  public SchedulerEvent Get(int id)  {    DateTime now = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(      DateTime.UtcNow, "Tokyo Standard Time");    // please search, here . . .    // (this api returns dummy data . . .)    SchedulerEvent resObj = new SchedulerEvent    {      Title = "Test Event " + id,      StartTime = now,      EndTime = now.AddHours(1)    };    return resObj;  }}public class SchedulerEvent{  public string Title;  public DateTime StartTime;  public DateTime EndTime;}. . .

いったんビルドして動作確認をおこなうと、既定で Json (application/json) が使用され、結果が返ってくるのがわかります。(下記)

GET http://localhost/api/scheduler/3  HTTP/1.1User-Agent: FiddlerHost: localhost
HTTP/1.1 200 OKServer: ASP.NET Development Server/10.0.0.0Date: Fri, 02 Mar 2012 08:50:07 GMTX-AspNet-Version: 4.0.30319Cache-Control: no-cachePragma: no-cacheExpires: -1Content-Type: application/json; charset=utf-8Connection: CloseContent-Length: 108{  "EndTime":"/Date(1330681807094+0900)/",  "StartTime":"/Date(1330678207094+0900)/",  "Title":"Test Event 3"}

また、下記の通り Accept ヘッダーを指定することで、Plain XML フォーマット (application/json) でデータを受け取ることもできます。

GET http://localhost/api/scheduler/3  HTTP/1.1User-Agent: FiddlerHost: localhostAccept: application/xml
HTTP/1.1 200 OKServer: ASP.NET Development Server/10.0.0.0Date: Fri, 02 Mar 2012 08:57:45 GMTX-AspNet-Version: 4.0.30319Cache-Control: no-cachePragma: no-cacheExpires: -1Content-Type: application/xml; charset=utf-8Connection: CloseContent-Length: 293<?xml version="1.0" encoding="utf-8"?><SchedulerEvent xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xmlns:xsd="http://www.w3.org/2001/XMLSchema">    <Title>Test Event 3</Title>    <StartTime>2012-03-02T17:57:45.6603939</StartTime>    <EndTime>2012-03-02T18:57:45.6603939</EndTime></SchedulerEvent>

 

Custom MIME Type によるシリアライズ

以上で、準備はできました。
では、上記のサービスを拡張し、カスタムのフォーマット (MIME Type) で結果を返してみましょう。

今回は、上記の SchedulerEvent オブジェクトを、Outlook の予定表ファイル (.ics) の形式として受け取るサンプル コードを作成してみましょう。
Outlook の予定表データ (.ics ファイル) は、下記のフォーマットを持つ単純なテキスト ファイルです。(下記は、「テストの予定」というサブジェクトで、開始時間が 2011/10/27 10:00、終了時間が 2011/10/27 10:30、15 分前にアラーム通知をおこなう場合の例です。)

BEGIN:VCALENDARPRODID:-//TESTApp/VERSION:2.0BEGIN:VTIMEZONETZID:Tokyo Standard TimeEND:VTIMEZONEBEGIN:VEVENTDTSTART;TZID="Tokyo Standard Time":20111027T100000DTEND;TZID="Tokyo Standard Time":20111027T103000SUMMARY;LANGUAGE=ja:テストの予定BEGIN:VALARMTRIGGER:-PT15MACTION:DISPLAYDESCRIPTION:ReminderEND:VALARMEND:VEVENTEND:VCALENDAR

この拡張をおこなう場合、ASP.NET Web API では、MediaTypeFormatter 継承クラスを作成して、独自のフォーマットを定義します。

まず、上記のプロジェクトに、クラスを追加します。今回、名前を AppointmentFormatter.cs とします。
新規作成された AppointmentFormatter.cs に、下記の通り、BufferedMediaTypeFormatter (MediaTypeFormatter から継承されています) を継承してコードを記述します。

補足 : 今回、BufferedMediaTypeFormatter を使用していますが、非同期で処理したい場合は、継承元の MediaTypeFormatter を使用します。

. . .using System.Web;using System.Net.Http.Formatting;using System.Net.Http.Headers;. . .public class AppointmentFormatter : BufferedMediaTypeFormatter{  public AppointmentFormatter() { ... }  public override object ReadFromStream(    Type type,    Stream stream,    HttpContentHeaders contentHeaders,    IFormatterLogger formatterLogger) { ... }  public override void WriteToStream(    Type type,    object value,    Stream stream,    HttpContentHeaders contentHeaders) { ... }  public override bool CanReadType(Type type) { ... }  public override bool CanWriteType(Type type) { ... }}. . .

カスタムの Formatter では、上記の通り、4 つの override メソッドを実装すれば OK です。
OnWriteToStream メソッドは、オブジェクトを MIME コンテンツのストリームに変換 (シリアライズ) するメソッドで、今回は、このメソッドを実装します。なお、OnReadFromStream メソッドは、逆に、コンテンツのストリームを .NET オブジェクトに逆シリアライズするメソッドで、このメソッドは、あとで実装します。
CanReadType / CanWriteType メソッドは、それぞれ、特定の型のオブジェクト (クラス) を扱うことが可能か否かを bool 値で指定します。(フレームワークは、実際のシリアライズ、逆シリアライズの前に、これらのメソッドを使って確認をおこないます。)
また、上記のコンストラクター (AppointmentFormatter() メソッド) を使って、インスタンスの作成時に、この MediaTypeFormatter がサポートしている MIME タイプを登録しておきます。(今回は、MIME タイプを text/calendar とします。)

以下の通り実装します。

. . .using System.IO;. . .public class AppointmentFormatter : BufferedMediaTypeFormatter{  public AppointmentFormatter()  {    // register custom MIME type    this.SupportedMediaTypes.Add(      new MediaTypeHeaderValue("text/calendar"));  }  public override object ReadFromStream(    Type type,    Stream stream,    HttpContentHeaders contentHeaders,    IFormatterLogger formatterLogger)  {    // we implement later ...    throw new NotImplementedException();  }  public override void WriteToStream(    Type type,    object value,    Stream stream,    HttpContentHeaders contentHeaders)  {    // serialize .NET object to .ics text format    SchedulerEvent item = value as SchedulerEvent;    StreamWriter writer = new StreamWriter(stream);    writer.WriteLine("BEGIN:VCALENDAR");    writer.WriteLine("PRODID:-//TESTApp/");    writer.WriteLine("VERSION:2.0");    writer.WriteLine("BEGIN:VTIMEZONE");    writer.WriteLine("TZID:Tokyo Standard Time");    writer.WriteLine("END:VTIMEZONE");    writer.WriteLine("BEGIN:VEVENT");    writer.WriteLine(      string.Format("DTSTART;TZID="Tokyo Standard Time":{0}",      item.StartTime.ToString("yyyyMMddTHHmmss")));    writer.WriteLine(      string.Format("DTEND;TZID="Tokyo Standard Time":{0}",      item.EndTime.ToString("yyyyMMddTHHmmss")));    writer.WriteLine(string.Format("SUMMARY;LANGUAGE=ja:{0}",      item.Title));    writer.WriteLine("BEGIN:VALARM");    writer.WriteLine("TRIGGER:-PT15M");    writer.WriteLine("ACTION:DISPLAY");    writer.WriteLine("DESCRIPTION:Reminder");    writer.WriteLine("END:VALARM");    writer.WriteLine("END:VEVENT");    writer.WriteLine("END:VCALENDAR");    writer.Flush();  }  public override bool CanReadType(Type type)  {    throw new NotImplementedException();  }  public override bool CanWriteType(Type type)  {    return (type == typeof(SchedulerEvent));  }}. . .

さいごに、上記のカスタムの Media Type Format (AppointmentFormatter) を ASP.NET Web API のフレームワークに登録します。
App_Start/WebApiConfig.cs を開き、下記 太字の通りコードを追加します。

. . .public static class WebApiConfig{  public static void Register(HttpConfiguration config)  {    . . .   config.Formatters.Add(new AppointmentFormatter());  }}. . .

補足 : MediaTypeFormatter の登録には、優先順位があります。(順位の高いものから先に評価されます。) MediaTypeFormatter を先頭に登録するには (評価を優先するには)、Insert メソッドを使用します。

以上で完了です。

Accept ヘッダーに text/calendar を指定してリクエストすると、下記の通り、.ics 形式の予定表のデータとして返ってくるようになります。

GET http://localhost/api/scheduler/3  HTTP/1.1User-Agent: FiddlerHost: localhostAccept: text/calendar
HTTP/1.1 200 OKServer: ASP.NET Development Server/10.0.0.0Date: Fri, 02 Mar 2012 09:23:05 GMTX-AspNet-Version: 4.0.30319Cache-Control: no-cachePragma: no-cacheExpires: -1Content-Type: text/calendarConnection: CloseContent-Length: 365BEGIN:VCALENDARPRODID:-//TESTApp/VERSION:2.0BEGIN:VTIMEZONETZID:Tokyo Standard TimeEND:VTIMEZONEBEGIN:VEVENTDTSTART;TZID="Tokyo Standard Time":20120302T182305DTEND;TZID="Tokyo Standard Time":20120302T192305SUMMARY;LANGUAGE=ja:Test Event 3BEGIN:VALARMTRIGGER:-PT15MACTION:DISPLAYDESCRIPTION:ReminderEND:VALARMEND:VEVENTEND:VCALENDAR

また、サービス側のコードに、下記 太字の通りコンテンツ タイプを指定すると、ブラウザーを使って http://localhost/api/scheduler/3 と入力した時にも、必ず、text/calendar の内容 (すなわち、上記の .ics の内容) が返ってくるようになります。

. . .public HttpResponseMessage Get(int id){  DateTime now = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(    DateTime.UtcNow, "Tokyo Standard Time");  // please search, here . . .  // (this api returns dummy data . . .)  SchedulerEvent resObj = new SchedulerEvent  {    Title = "Test Event " + id,    StartTime = now,    EndTime = now.AddHours(1)  };  HttpResponseMessage resMsg =    new HttpResponseMessage(HttpStatusCode.OK);  resMsg.Content =    new ObjectContent<SchedulerEvent>(resObj,      new AppointmentFormatter());  return resMsg;}. . .

このため、Outlook のインストールされたクライアント (PC) のブラウザーから この URL に接続すると、下図のように、Outlook の予定追加の画面が (ブラウザーから) 起動します。

補足 : 上記では、ContentType を強制的に上書きしていますが、現実の開発では、他のフォーマット (Json など) もそのまま使用できるようにカスタマイズしておくと良いでしょう。例えば、ASP.NET Web API の DelegatingHandler を使って、Path から Uri Mapping に変換するカスタム ハンドラーを登録する方法があります。(例えば、http://localhost/api/scheduler/3/ics のような URL を入力することで、ブラウザーを使って .ics フォーマットのファイルをダウンロードできます。)

このように、ASP.NET Web API を使用すると、HTTP 固有のフォーマット処理など システム的な処理 (関心事) をメインのロジックと分離し、かつ、再利用して構築できます。

 

Custom MIME Type による逆シリアライズ

上記では、.NET のオブジェクト (SchedulerEvent) からカスタム MIME のコンテンツに変換しましたが、逆に、HttpClient などを使って、受信した MIME のコンテンツ (HTTP の Headers、Body) を .NET オブジェクトに逆シリアライズできます。

上記とは逆に、今後は、MediaTypeFormatter (BufferedMediaTypeFormatter) の OnReadFromStream メソッドを下記 (太字) の通り実装してみます。

. . .public class AppointmentFormatter : BufferedMediaTypeFormatter{  . . .  public override object ReadFromStream(    Type type,    Stream stream,    HttpContentHeaders contentHeaders,    IFormatterLogger formatterLogger)  {    object result = null;    if (string.Equals(contentHeaders.ContentType.MediaType, "text/calendar"))    {      SchedulerEvent resItem = new SchedulerEvent();      StreamReader reader = new StreamReader(stream);      while (!reader.EndOfStream)      {        string contentsLine = reader.ReadLine();        string[] contentsVals = contentsLine.Split(':');        if (contentsVals[0].StartsWith("SUMMARY;"))          resItem.Title = contentsVals[1];        else if (contentsVals[0].StartsWith("DTSTART;"))          resItem.StartTime = DateTime.ParseExact(contentsVals[1],            "yyyyMMddTHHmmss", null);        else if (contentsVals[0].StartsWith("DTEND;"))          resItem.EndTime = DateTime.ParseExact(contentsVals[1],            "yyyyMMddTHHmmss", null);      }      result = resItem;    }    return result;  }  public override bool CanReadType(Type type)  {    return (type == typeof(SchedulerEvent));  }  . . .}. . .

以上で完了です。では、逆シリアライズの実験をしてみましょう。
かなり強引ですが、上記の ASP.NET MVC のプロジェクト (コントローラー) から、下記の通り、HttpClient を使用して GetEvent を呼び出してみます。この際、Accept ヘッダーにより text/calendar の MIME フォーマットでデータを受信しますが、最終的に、下記の item には、SchedulerEvent オブジェクト (.NET オブジェクト) として結果を取得できます。(以降は、.NET のプログラムから、オブジェクトとしてこのコンテンツの内容を確認できます。)

. . .using System.Net.Http;using System.Net.Http.Headers;. . .public class HomeController : Controller{    public ActionResult Test()    {        string serviceUrl = string.Format("{0}://{1}{2}",                    HttpContext.Request.Url.Scheme,                    HttpContext.Request.Url.Authority,                    Url.Content("~/api/scheduler/3"));        HttpClient cl = new HttpClient();        cl.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/calendar"));        HttpResponseMessage res = cl.GetAsync(serviceUrl).Result;        string contentBody = res.Content.ReadAsStringAsync().Result;        SchedulerEvent item = res.Content.ReadAsAsync<SchedulerEvent>(new[] { new AppointmentFormatter() }).Result;        . . .        return View();    }    . . .

補足 : 実際の開発では、カスタムの MediaTypeFormatter クラスをライブラリーとして作成するなどして、クライアント側でもこのクラスを使用 (再利用) できるようにしておくと良いでしょう。ここでは、単純に、同じ ASP.NET MVC プロジェクトのコントローラー コードでテストしています。(真似しないでください。。。)

 

なお、MediaTypeFormatter から継承された標準のフォーマッターとして、JsonMediaTypeFormatter、XmlMediaTypeFormatter、FormUrlEncodedMediaTypeFormatter などが提供されています。

 

Categories: Uncategorized

Tagged as:

9 replies»

Leave a Reply