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

arch by Takekazu Omi, on Flickr

.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見ると非常に参考になる。

../../../_images/2013_12_08_scrn-001.png

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>なので)ので日和ってしまった。