OWIN - Open Web Interface for .NET を使う
OWIN(Open Web Interface for .NET)は、.NET Framework の WebサーバとWebアプリケーション接続するためのインタフェースであり、新しい HTTP Serverのプログラミング抽象化レイヤーを定義するものだ。2010年の終わりのころにBenjamin van der Veen 氏が始め、Draft 7 12 July 2012では、Author: OWIN working group となっている。参照:http://owin.org/spec/history-1.0.html
.NETでは、HTTP Serverのプログラミング抽象化レイヤーは、ASP.NETの初期のことに構築されてその後ほぼ変らずに今まで来た、An Overview of Project Katana August 30, 2013 Howard Dierkingに、初めのころの話としてASP.NET設計時のターゲットの話が書いあり実に面白い。
これによると、当時(ASP.NETの初期設計時)の主なターゲットは、「Classic ASPを使っている人」と、「VB6等でWindows で業務アプリを書いている人」にWebプラットフォームプログラミングを提供することで、.NET Frameworkの一部としてリリースされるということもありあまり時間も無い中で作られたらしい。
その結果出来上がったのが、従来のVB6アプリの習慣に沿ったイベントモデルをベースにしたWeb Formsのアーキテクチャーと論理的に異なる HTTP ObjectとWeb Forms FrameworkがタイトにカップルされたSystem.Web.dll らしい。
昔を振り返ってみると、1993年の終わりころ[1]UCSA HTTPdにCGI が現れ、1996年には Windows NT 4.0 Option Pack で ASPが登場、1997年には、Java Servlet が出ている。ASP.NETのリリースは2002年なので、ASPのリリースから6年たってほぼ同じモデルを踏襲した設計になっているということになる。今は更に11年後、ASPから数えると17年経ってる、変わらないのは資産の継承という点では良い面もあるが、その間蓄積された知識が十分生かされているかどうかとかんがえるとちょっと期間が長すぎたような気もする。
その間Rubyを始めとする他のプラットフォームは新しいデザインを模索しており、OWINの発想のもとになっていると言われているRack: a Ruby Webserver Interface Feb 2007が生まれる。こちらは(Ruby)は、.NET と事情が違って、標準的なWebサーバとWebアプリケーション接続するためのインタフェースが存在しない中数多くのWeb サーバー、フレームワークが存在する問題への解法としてRackが生まれている。
.NETでは最初に標準的なWeb Server(IIS)と、Framework(System.Web.dll)ありきで始まったため混乱は無かったが自由な発展が阻害され、一方Ruby/Pythonなど標準的なものが無い中では混沌のなかから優れた標準(Rack/WSGI)が生まれたというのは実に面白い。
OWINの基本
The Open Web Interface for .NET (OWIN) の主要なデータ構造は2つしか無い。ひとつは、環境を保持する environment dictionary これに、HTTP request and response を処理するのに必要なデータは保持される。
IDictionary<string, object>
2つ目は、application delegate 全てのコンポーネントの間は下記の function signature で呼ばれる。
Func<IDictionary<string, object>, Task>;
Headers、Request Body、Response Body などの抽象度の高いオブジェクトを、この上に構築している。ちょっと中を見てみた感じでは、OWIN自体は非常にシンプルな構成[2]でコンポーネント指向も高くいい感じで使えそうだ。とりあえずなにか、OWIN Middleware を作ってみようと思ったけどネタが思い付かない。どうしようかと思っていたら、neuecc/Owin.RedisSessionなんてものを見つけ「ああOWINだとSessionすら無いのか」と気が付いて Azure Cache 版を作ってみることにした。
OWIN Middleware Azure Cache Session
そんなわけで、Azure Cache に Session を保存するOWIN Middleware を作成した。コードはGitHubにあるOWIN Azure Cache Session Middleware手探りで作った習作だが、簡単に中身を説明する。OWIN Middleware を使って下記のような構成にする。Azure CacheとSessionのMiddlewareはブラウザとアプリケーションの間にフィルタのように入る。この手のパターンは便利でWeb Application Frameworkでは随所に出てくる。今回、CacheとSessionで分ける必要があるか迷ったが、書いてみたら分けた方がシンプルになったので分けてある。
OWIN SelfHost Applicationの作成
まずは試しで、OWIN Selft Host環境を作って書いてみる。
Console Projectを作成して必要なパッケージを入れる
nugetから必要なパッケージを入れる
install-package Microsoft.Owin.Hosting
install-package Microsoft.Owin.Host.HttpListener
install-package Microsoft.Owin.Diagnostics
install-Package Owin.Extensions
Program.csのmainを下記のようにする
WebApp.Start<Startup>(url)とするとlistnerを上げて、Startup Classにリクエストを回してくれる。今回の設定だと、Microsoft.Owin.Host.HttpListener が入っているのでHttpListnerを使ったSelfHostになる。
using System;
using Microsoft.Owin.Hosting;
namespace SelfHostSample
{
class Program
{
static void Main(string[] args)
{
string uri = args.Length == 0 ? "http://localhost:8081/" : args[0];
using (WebApp.Start<Startup>(uri))
{
Console.WriteLine("Started");
Console.ReadKey();
Console.WriteLine("Stopping");
}
}
}
}
Startup用のクラスを追加する
[assembly: OwinStartup(typeof(SelfHostSample.Startup))]でアプリケーションのクラスを登録する。登録されたクラスのConfiguration MethodがWebApp.Start()で実行される。
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(SelfHostSample.Startup))]
namespace SelfHostSample
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseWelcomePage();
}
}
}
これで動かすとこんな感じになる
このコードでは、app.UseWelcomePage();としているので、Welcomeページが表示される。このあたりの実装は、katanaproject.codeplex.com WelcomePageMiddleware見ると非常に参考になる。
Azure Cache Session Middleware
ざっと手順を説明して、コード上のポイントを解説する。例外処理周りなどは検討の余地が多い。
Windows Azure Cache Client を入れる
Install-Package Microsoft.WindowsAzure.Caching
App.config内のAzure Cacheの設定をする
App.config 内の configuration/dataCacheClients/dataCacheClient/autoDiscover のidentifier属性をAzure CacheのEndpointにして、securityProperties/messageSecurityのauthorizationInfo属性にManage Access Keys を設定する。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="dataCacheClients" type="Microsoft.ApplicationServer.Caching.DataCacheClientsSection, Microsoft.ApplicationServer.Caching.Core" allowLocation="true" allowDefinition="Everywhere" />
<section name="cacheDiagnostics" type="Microsoft.ApplicationServer.Caching.AzureCommon.DiagnosticsConfigurationSection, Microsoft.ApplicationServer.Caching.AzureCommon" allowLocation="true" allowDefinition="Everywhere" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<dataCacheClients>
<dataCacheClient name="default">
<!--To use the in-role flavor of Windows Azure Cache, set identifier to be the cache cluster role name -->
<!--To use the Windows Azure Cache Service, set identifier to be the endpoint of the cache cluster -->
<autoDiscover isEnabled="true" identifier="********.cache.windows.net" />
<!--<localCache isEnabled="true" sync="TimeoutBased" objectCount="100000" ttlValue="300" />-->
<!--Use this section to specify security settings for connecting to your cache. This section is not required if your cache is hosted on a role that is a part of your cloud service. -->
<securityProperties mode="Message" sslEnabled="false">
<messageSecurity authorizationInfo="*************************" />
</securityProperties>
</dataCacheClient>
</dataCacheClients></configuration>
OWIN Middleware用のプロジェクトを追加する
今回は、Owin.Middleware という名前で作成して、Azure CacheとSessnionのMiddlewareを作成する。Owin.Middleware project必要なパッケージを追加する。このプロジェクトではOWIN Hosting系のパッケージは入れない。
install-Package Microsoft.Owin
install-Package Microsoft.WindowsAzure.Caching
install-Package EnterpriseLibrary.TransientFaultHandling.Caching
Azrue Cache Middleware
OWIN Middleware 定番クラスを3つ追加する。Middleware が処理の本体、Optionsは、Middlewareのオプション、Extensionsは拡張メソッドが入っている。今回は、素のOwinではなく、Microsoft.Owin を使っている。Microsoft.Owin は、Microsoftが作成したOwinの薄いラッパである程度型割り当て済みのデータを渡してくれるのでコーディングが楽になる。[3]
using System;
using System.Threading.Tasks;
using Microsoft.ApplicationServer.Caching;
using Microsoft.Owin;
namespace Owin.Middleware
{
public class AzureCacheMiddleware : OwinMiddleware
{
public const string CacheKeyName = "Kyrt.CacheKeyName";
private readonly AzureCacheOptions _options;
public AzureCacheMiddleware(OwinMiddleware next, AzureCacheOptions options) : base(next)
{
_options = options ?? new AzureCacheOptions();
}
public override Task Invoke(IOwinContext context)
{
try
{
object cache;
if (!context.Environment.TryGetValue(CacheKeyName, out cache))
{
cache = new AzureCacheClient(_options.CacheName);
context.Environment[CacheKeyName] = cache;
}
}
catch (DataCacheException e)
{
context.TraceOutput.WriteLine(e);
}
catch (Exception ex)
{
context.TraceOutput.WriteLine(ex);
throw;
}
return Next.Invoke(context);
}
}
}
Middlewareの基本的な考えは非常に簡単で Invoke の処理内で次のInvokeの前後に割り込んで処理をするだけ。AzureCacheMiddlewareでは、AzureCacheClient(中身はDataCache)を作成してEnvironmentに追加している。このケースでは、Invokeの前に処理を入れただけで、Invoke後は何もしていない。
オプションや拡張メソッドのクラスはおまけのようになもので大したことはしていない。
namespace Owin.Middleware
{
public class AzureCacheOptions
{
public AzureCacheOptions()
{
CacheName = null;
}
public string CacheName { get; set; }
}
}
using System;
using Microsoft.Owin;
using Owin;
namespace Owin.Middleware
{
public static class AzureCacheExtensions
{
public static IAppBuilder UseAzureCache(this IAppBuilder builder, AzureCacheOptions options = null)
{
if (builder == null)
{
throw new ArgumentNullException("builder");
}
return builder.Use(typeof (AzureCacheMiddleware), options);
}
public static AzureCacheClient Cache(this IOwinContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
return context.Environment[AzureCacheMiddleware.CacheKeyName] as AzureCacheClient;
}
}
}
Session Middlewareは、Invoke前処理でCacheからのSessionの復元とResponseのCookieへのセッションIDの設定、後処理でSessionの保存を行っている。
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
namespace Owin.Middleware
{
public class SessionMiddleware : OwinMiddleware
{
public const string SessionKeyName = "Kyrt.Session";
public SessionMiddleware(OwinMiddleware next) : base(next)
{
}
public override Task Invoke(IOwinContext context)
{
string sessionId = null;
try
{
sessionId = AzureCacheSessionProvidor.PreInvoke(context, SessionKeyName);
}
catch (Exception e)
{
context.TraceOutput.WriteLine(e);
}
return Next.Invoke(context).ContinueWith((task, state) =>
{
try
{
var p = state as Tuple<IOwinContext, string>;
if (p!=null && p.Item2 != null)
AzureCacheSessionProvidor.PostInvoke( p.Item1, SessionKeyName, p.Item2);
}
catch (Exception e)
{
context.TraceOutput.WriteLine(e);
}
return task;
}, Tuple.Create(context, sessionId));
}
}
}
アプリケーションの変更
最後に、アプリケーションをCacheとSessionを使うように変更する。
using System;
using System.Collections.Generic;
using Microsoft.Owin;
using Owin;
using Owin.Middleware;
[assembly: OwinStartup(typeof(SelfHostSample.Startup))]
namespace SelfHostSample
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseAzureCache();
app.UseSession();
app.Run(async context =>
{
context.TraceOutput.WriteLine("start app.Run {0}", context.Request.Path);
context.Response.ContentType = "text/html";
try
{
var time = context.Cache().GetOrAdd("first time", s => DateTimeOffset.Now);
var count = context.Cache().Increment("counter", 1, 0);
int sessionCount = context.Session().Get("sessionCount", -1);
sessionCount++;
context.Session()["sessionCount"] = sessionCount;
var msg = string.Format("Hello, World! {0} {1}/{2} {3}<br>", time.ToString(), sessionCount, count, context.Request.Path);
await context.Response.WriteAsync(msg);
}
catch (Exception e)
{
context.TraceOutput.WriteLine(e);
}
});
}
}
}
まとめ
OWINは、シンプルで柔軟なHTTP 抽象化レイヤーを提供してくれる。Middlewareのinvoke chain の仕組みと拡張可能なEnvironmetはシンプルだた強力だ。katanaproject.codeplex.comを見ると認証系、View Engine、Compressionなどのmiddlewareが散見され、それぞれのコードは興味深い。ただ、現時点ではアプリケーションの構築プラットフォームとして使うには道具立てが足りないようだ。でも今回のようにAzure Cache Session Provider などを書いてみると、パフォーマンス的な問題や実装上の課題などが見えてきてなかなか勉強になるし、ブレイクスルーできるような点も見えてくる。少々フロンティア的な色が強いが挑戦する価値のある分野だと思う。
[1] | Server Scripts, by Rob McCool, www-talk mailing list, Sun, 14 Nov 1993 |
[2] | Katana Project のコードを見ると重量級で途方にくれる。System.Web との相互運用性をもたせようとして難しいことになっているらしい。 |
[3] | 生 Owin だと型の情報がほどんど無い(IDictionary<string, object>なので)ので日和ってしまった。 |
Windows Azure Storage Client 3.0.1
2013/12/11 Windows Azure Storage Client 3.0.1 がリリースされました。変更内容はBUG FIXのみです。nuget:Windows Azure Storage 3.0.1
修正点 3.0.1.0:
- All (WP): Get/SetACL で明示的に Accept type application/xml を設定
- Blobs: Lease operations の後で、LastModified と ETag プロパティを設定
- Tables: Nuget package に Microsoft.Data.Services.Client への明示的な参照を追加
- Tables: Json .NET の bug に起因したtable query responseのパース時の問題を修正。Json .NET bugの詳細:http://james.newtonking.com/archive/2013/11/29/fixing-jarray-getenumerator-method-not-found-bug
- Tables (RT): クエリーと列挙操作の継続トークンに関する問題を修正
メモ
Json .NET の bug から発生している問題は、Json .NETの5.0.5 で JArray.GetEnumerator をpublicにした結果、IEnumerableを取った時に、今までIEnumerable<JToken>)が返ってたのがJArray.GetEnumerator になってしまったことに起因するようです。5系の中で非互換な修正をしてしまったので、storageが使うJson .NETと他の部分で使うのが混在したときに両立出来ずにお手上げになってしまう可能性があったそうですが、このバージョンで解決されます。
Windows Azure PowerShell 0.7.2 リリース
Windows Azure PowerShell 0.7.2 がリリースされました。 10月の 0.7.0 、11月の 0.7.1 続く12月リリースです。最近毎月リリースされています。Azureに新機能が出ると追っかけでcmdletが追加されます。| 主な変更は、HDInsight cmdletsの追加、Web Site、VM cmdletの改善、Virtual IP reservation、Cloud Service cmdletの Visual Studio 互換です。
インストール
最新版はWeb Platform Installer 経由で入れられます。12/10 リリースのWindows Azure Poershell を選択してください。
インストールしたら念のためバージョンを確認します。Azureの所が、0.7.2ですね。
$ Get-Module | ft name,version
Name Version
---- -------
Autoload 0.0
Azure 0.7.2
Microsoft.PowerShell.Management 3.1.0.0
Microsoft.PowerShell.Utility 3.1.0.0
posh-git 0.0
PsEnv 0.0
PSReadline 1.0.0.1
変更点
- HDInsight cmdlets
- Add-AzureHDInsightConfigValues
- Add-AzureHDInsightMetastore
- Add-AzureHDInsightStorage
- Get-AzureHDInsightCluster
- Get-AzureHDInsightJob
- Get-AzureHDInsightJobOutput
- Get-AzureHDInsightProperties
- New-AzureHDInsightCluster
- New-AzureHDInsightClusterConfig
- New-AzureHDInsightHiveJobDefinition
- New-AzureHDInsightMapReduceJobDefinition
- New-AzureHDInsightPigJobDefinition
- New-AzureHDInsightSqoopJobDefinition
- New-AzureHDInsightStreamingMapReduceJobDefinition
- Remove-AzureHDInsightCluster
- Revoke-AzureHDInsightHttpServicesAccess
- Set-AzureHDInsightDefaultStorage
- Start-AzureHDInsightJob
- Stop-AzureHDInsightJob
- Use-AzureHDInsightCluster
- Wait-AzureHDInsightJob
- Grant-AzureHDInsightHttpServicesAccess
- Invoke-AzureHDInsightHiveJob
- Web Site の WebSocket と managed pipe mode の設定
- Set-AzureWebsite -WebSocketEnabled -ManagedPipelineMode
- Web Site の remote debugging 設定
- Enable-AzureWebsiteDebug -Version
- Disable-AzureWebsiteDebug
- VM を削除した時の VHD cleaning up オプション
- Remove-AzureVM -DeleteVHD
- Remove-AzureService -DeleteAll
- Remove-AzureDeployment -DeleteVHD
- 仮想 IP 予約 (Virtual IP reservation) preview feature (in AzurePreview module)
- Get-AzureDeployment
- Get-AzureReservedIP
- New-AzureReservedIP
- New-AzureVM
- Remove-AzureReservedIP
- 下記の cmdletsでの Visual Studio Cloud Service プロジェクトのサポート
- Start-AzureEmulator
- Publish-AzureServiceProject
- Save-AzureServiceProjectPackage
最後に
今回の目玉は、HDInsight cmdletsかなと思いますが、個人的にはVisual Studioで作ったプロジェクトが使えるようになったのが一番嬉しいですね。
Windows Azure Table の JSON payload
2013/11/27 に公開された最新のWindows Azure Storageでは新しくJSON Payload Format が導入されました。変更点の詳細が Windows Azure Storage Team Blog のWindows Azure Tables: Introducing JSONに出ています、興味深い内容です。
翻訳は追々出ると思うので、拾い読みしながら気になったことを書きます。
- [引用]JSON Payloadは、version “2013-08-15”の一部してリリースされました。JSONは従来の AtomPub[1]フォーマットの OData payload format に比べて著しくサイズが小さくなり latency が低くなっています。また、payload size を削減するため、 insert の payload echo を Off にする方法を提供します。これらの新機能は、新しいWindows Azure Storage Client 3.0ではデフォルトの機能として働きます。
従来の HTTP request/response を見ていると無駄が目立ったので、それが削減されるのは大歓迎です。ここには、version “2013-08-15”でのplayloadの削減が書いてありますが、2013年11月(先月)ぐらいから、version “2012-02-12” の AtomPub でも余計な改行や空白を削減するなどの変更が行われています。このあたりの変更については、Windows Azure Tables の Breaking Changes 2013/11を見て下さい。
Insert の payload echo を Off にする話が出ていますが、version “2011-08-18” でサポートされたInsert Or Merge Entity (REST API)ではレスポンスのBODYが空でステータスコードは204 (No Content) を返すようになっていました、これを insert でも同じように動作するモードを付けたということのようです。Insertの時にサーバー側で付与される情報はETagとTimestampだけで、それ以外は送信した内容と同じです、レスポンスヘッダーにETagは返ってくるので、エンティティ全体が帰ってこなくても困ることはほとんど無いと思います。場合によっては、Timestamp を使う場合があるかもしれませんが、その場合は設計を見直すか、エンティティ全体を返すモードで使うかになります。
What is JSON
- [引用]JSON(JavaScript Object Notation) は、構造化データをシリアライズするための lightweight text format です。AtomPubと同じように、 OData extends JSON formatでは、エンティティとプロパティの一般的な規則を定義します。AtomPubは異なり、OData JSON では response payload が一部がペイロードサイズを低減するために省略されます。そのため、受信側でlink, type と control dataなどのデータを再構成ための表現力が不足しています。OData には下記のような複数の JSON フォーマットがあります:
ちょっと分かりづらいです(訳が悪いのでしょうか)、この辺りの考えは、OData JSON Format Version 4.0 2 Candidate OASIS Standard 01 の 2.JSON Format Designがわかりやすく考え方を説明してくれています。それによると、実際のpayloadからwire formatの予測可能な部分を取り除いて送信できるようにするという考えでデザインされているそうです。クライアントがデータに関するメタデータを持っているというシナリオでは毎回のpayloadに全メタ情報を載せて送る(以前のAtomPubのように)のは無駄なので省略できるようにしますという話です。どこまでメタデータを持たせるかは nometadata、minimalmetadata、fullmetadata の3段階用意しています。あとそれに伴って、name/valueのペアに順序の制約を付けています。順序の話は、Hashにそのまま読み込めなくなるので少々面倒な制約です。IDLやXML Schema のようなメタ情報を定義する仕組みを持ち込まずに、受信側のクライアントのコードで実装もしくは、JSON内にメタ情報埋め込みという話にしたのは、JSONの手軽さを損なわないという点で評価できます。
nometadata、minimalmetadata、fullmetadata や、JSON payload の詳細についてはPayload Format for Table Service Operationsを見て下さい。
AtomPub vs JSON format
具体的に、AtomPubとJSONを比較してみます。JSONにすると、基本的に閉じタグが無くなるので、それだけでもかなりの削減になるのはすぐわかります。AtomPub [引用]と、JSON nometadata [引用]を見ると一目瞭然です。AtomPub [引用]の方は、元々、Atom Publishing Protocolもので、インターネット上でのコンテンツの交換を目的として設計されたものです。Table Storageでは使われない、冗長なタグが散見されます。title、id、link、author あたりとか、namespace は不要なので、それも外してしまうと結構スッキリします。試しに、XMLで余計なタグを削ったXML nometadata (参考までに作ってみました)というのを作ってみました。閉じタグだけはなんとも成らないですが、結構シンプルになります。これを見ると、XMLを選択した功罪というより AtomPub を選択した問題の方が大きかったことがわかります。OData/WCFの流れに拘りすぎてちょっと遠回りしてしまったようです。
Payloadの違いによる比較のテーブルが面白いので引用します
Format | Request Header Size | Request Body Size | Response Header Size | Response Body Size | % Savings in HTTP Body Size only vs. AtomPub | % Savings in total HTTP transfer vs. AtomPub |
---|---|---|---|---|---|---|
AtomPub | 3,611 | 2,861 | 3,211 | 8,535 | N/A | N/A |
JSON MinimalMetadata | 3,462 | 771 | 3,360 | 2,529 | 71% | 44% |
JSON NoMetadata | 3,432 | 771 | 3,330 | 1,805 | 77% | 49% |
AtomPub [引用]
<?xml version="1.0" encoding="utf-8"?>
<feed xml:base="http://someaccount.table.core.windows.net/" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
<id>http://someaccount.table.core.windows.net/Customers</id>
<title type="text">Customers</title>
<updated>2013-12-03T06:37:21Z</updated>
<link rel="self" title="Customers" href="Customers" />
<entry m:etag="W/"datetime'2013-12-03T06%3A37%3A20.9709094Z'"">
<id>http://someaccount.table.core.windows.net/Customers(PartitionKey='Jonathan',RowKey='Foster')</id>
<category term="someaccount.Customers" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" title="Customers" href="Customers(PartitionKey='Jonathan',RowKey='Foster')" />
<title />
<updated>2013-12-03T06:37:21Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
<d:PartitionKey>Jonathan</d:PartitionKey>
<d:RowKey>Foster</d:RowKey>
<d:Timestamp m:type="Edm.DateTime">2013-12-03T06:37:20.9709094Z</d:Timestamp>
<d:Address>1234 SomeStreet St, Bellevue, WA 75001</d:Address>
<d:Email>Jonathan@fourthcoffee.com</d:Email>
<d:PhoneNumber>425-555-0101</d:PhoneNumber>
<d:CustomerSince m:type="Edm.DateTime">2005-01-05T00:00:00Z</d:CustomerSince>
<d:Rating m:type="Edm.Int32">3</d:Rating>
</m:properties>
</content>
</entry>
<entry m:etag="W/"datetime'2013-12-03T06%3A37%3A21.1259249Z'"">
<id>http://someaccount.table.core.windows.net/Customers(PartitionKey='Lisa',RowKey='Miller')</id>
<category term="someaccount.Customers" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" title="Customers" href="Customers(PartitionKey='Lisa',RowKey='Miller')" />
<title />
<updated>2013-12-03T06:37:21Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
<d:PartitionKey>Lisa</d:PartitionKey>
<d:RowKey>Miller</d:RowKey>
<d:Timestamp m:type="Edm.DateTime">2013-12-03T06:37:21.1259249Z</d:Timestamp>
<d:Address>4567 NiceStreet St, Seattle, WA 54332</d:Address>
<d:Email>Lisa@northwindtraders.com</d:Email>
<d:PhoneNumber>425-555-0101</d:PhoneNumber>
<d:CustomerSince m:type="Edm.DateTime">2003-01-05T00:00:00Z</d:CustomerSince>
<d:Rating m:type="Edm.Int32">2</d:Rating>
</m:properties>
</content>
</entry>
<entry m:etag="W/"datetime'2013-12-03T06%3A37%3A20.7628886Z'"">
<id>http://someaccount.table.core.windows.net/Customers(PartitionKey='Walter',RowKey='Harp')</id>
<category term="someaccount.Customers" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<link rel="edit" title="Customers" href="Customers(PartitionKey='Walter',RowKey='Harp')" />
<title />
<updated>2013-12-03T06:37:21Z</updated>
<author>
<name />
</author>
<content type="application/xml">
<m:properties>
<d:PartitionKey>Walter</d:PartitionKey>
<d:RowKey>Harp</d:RowKey>
<d:Timestamp m:type="Edm.DateTime">2013-12-03T06:37:20.7628886Z</d:Timestamp>
<d:Address>1345 Fictitious St, St Buffalo, NY 98052</d:Address>
<d:Email>Walter@contoso.com</d:Email>
<d:PhoneNumber>425-555-0101</d:PhoneNumber>
<d:CustomerSince m:type="Edm.DateTime">2010-01-05T00:00:00Z</d:CustomerSince>
<d:Rating m:type="Edm.Int32">4</d:Rating>
</m:properties>
</content>
</entry>
</feed>
JSON minimalmetadata [引用]
{
"odata.metadata":"http://someaccount.table.core.windows.net/$metadata#Customers",
"value":[
{
"PartitionKey":"Jonathan",
"RowKey":"Foster",
"Timestamp":"2013-12-03T06:39:56.6443475Z",
"Address":"1234 SomeStreet St, Bellevue, WA 75001",
"Email":"Jonathan@fourthcoffee.com",
"PhoneNumber":"425-555-0101",
"CustomerSince@odata.type":"Edm.DateTime",
"CustomerSince":"2005-01-05T00:00:00Z",
"Rating":3
},
{
"PartitionKey":"Lisa",
"RowKey":"Miller",
"Timestamp":"2013-12-03T06:39:56.7943625Z",
"Address":"4567 NiceStreet St, Seattle, WA 54332",
"Email":"Lisa@northwindtraders.com",
"PhoneNumber":"425-555-0101",
"CustomerSince@odata.type":"Edm.DateTime",
"CustomerSince":"2003-01-05T00:00:00Z",
"Rating":2
},
{
"PartitionKey":"Walter",
"RowKey":"Harp",
"Timestamp":"2013-12-03T06:39:56.4743305Z",
"Address":"1345 Fictitious St, St Buffalo, NY 98052",
"Email":"Walter@contoso.com",
"PhoneNumber":"425-555-0101",
"CustomerSince@odata.type":"Edm.DateTime",
"CustomerSince":"2010-01-05T00:00:00Z",
"Rating":4
}
]
}
JSON nometadata [引用]
{
"value":[
{
"PartitionKey":"Jonathan",
"RowKey":"Foster",
"Timestamp":"2013-12-03T06:45:00.7254269Z",
"Address":"1234 SomeStreet St, Bellevue, WA 75001",
"Email":"Jonathan@fourthcoffee.com",
"PhoneNumber":"425-555-0101",
"CustomerSince":"2005-01-05T00:00:00Z",
"Rating":3
},
{
"PartitionKey":"Lisa",
"RowKey":"Miller",
"Timestamp":"2013-12-03T06:45:00.8834427Z",
"Address":"4567 NiceStreet St, Seattle, WA 54332",
"Email":"Lisa@northwindtraders.com",
"PhoneNumber":"425-555-0101",
"CustomerSince":"2003-01-05T00:00:00Z",
"Rating":2
},
{
"PartitionKey":"Walter",
"RowKey":"Harp",
"Timestamp":"2013-12-03T06:45:00.5384082Z",
"Address":"1345 Fictitious St, St Buffalo, NY 98052",
"Email":"Walter@contoso.com",
"PhoneNumber":"425-555-0101",
"CustomerSince":"2010-01-05T00:00:00Z",
"Rating":4
}
]
}
XML nometadata (参考までに作ってみました)
<?xml version="1.0" ?>
<Feed>
<Properties>
<PartitionKey>Jonathan</PartitionKey>
<RowKey>Foster</RowKey>
<Timestamp>2013-12-03T06:39:56.6443475Z</Timestamp>
<Address>1234 SomeStreet St, Bellevue, WA 75001</Address>
<Email>Jonathan@fourthcoffee.com</Email>
<PhoneNumber>425-555-0101</PhoneNumber>
<CustomerSince>2005-01-05T00:00:00Z</CustomerSince>
<Rating>3</Rating>
</Properties>
<Properties>
<PartitionKey>Lisa</PartitionKey>
<RowKey>Miller</RowKey>
<Timestamp>2013-12-03T06:39:56.7943625Z</Timestamp>
<Address>4567 NiceStreet St, Seattle, WA 54332</Address>
<Email>Lisa@northwindtraders.com</Email>
<PhoneNumber>425-555-0101</PhoneNumber>
<CustomerSince>2003-01-05T00:00:00Z</CustomerSince>
<Rating>2</Rating>
</Properties>
<Properties>
<PartitionKey>Walter</PartitionKey>
<RowKey>Harp</RowKey>
<Timestamp>2013-12-03T06:39:56.4743305Z</Timestamp>
<Address>1345 Fictitious St, St Buffalo, NY 98052</Address>
<Email>Walter@contoso.com</Email>
<PhoneNumber>425-555-0101</PhoneNumber>
<CustomerSince>2010-01-05T00:00:00Z</CustomerSince>
<Rating>4</Rating>
</Properties>
</Feed>
最後に
書き始めたら案外知らないことが多く。てっきり、OData V3 の JSON[2]、Content-Type: application/json;odata=verbose、json light (content-type : application/json; odata=light)が使われているのかと思ったら、いつの間にかOData Version 4.0 JSON[3]なんてものがあって、そっちが使われていたことに気が付いたり。
Windows Azure Storage Client 3.0で使っている、ODataLib 5.6.0から OData V4をサポートしているらしい[4]ということが分かったりということで手間取りました。
じゃあ、ODataLib 5.6.0 のコードちょっと見ておくかと思ったら、odata.codeplex.com の ODataLib は2011年から放置状態になっていて(これは知ってました)、http://www.symbolsource.org/Public/Metadata/NuGet/Project/Microsoft.Data.Services/5.6.0からコードが確認できるってことに気が付いたりで手間取りました。
今回は前半しか紹介できていないので、そのうち続きを書きたいと思います。
Resources
[1] | AtomPub |
[2] | OData V3 JSON Verbose Format |
[3] | OData JSON Format Version 4.0 Candidate OASIS Standard 01 19 November 2013 |
[4] | OData V4 のstackが、ODataLib として提供されることがコメントに書いてあるWCF Data Services 5.6.0 Release |