Windows AzureとMEFで再デプロイを必要としない拡張(または縮小)可能なクラウドアプリケーション。BlobストレージからMEFのパーツを検索できるカスタムカタログ BlobStorageCatalog を作ろう。
しばらくF#ネタしか書いていませんでした。たまにはC#ネタを置いておきます。
C#ネタというよりは、最近仕事で利用しているWindows AzureとMEFのネタですが。
Managed Extensibility Framework (MEF)とは
Managed Extensibility Framework (MEF) は、.NET Frameworkに最適な拡張可能なアプリケーションを作成するための一連の機能を提供する軽量ライブラリです。既定で提供されているのは属性プログラミングモデルですが、MEFのコアAPIは全く属性に依存していないので、MEFのコアAPIにアクセスする方法を定義したクラスを独自に作成することでリフレクションベースのプログラミングモデルなど、様々なスタイルで利用可能です。
MEFは、.NET Framework 4 およびSilverlight 4で標準利用可能です。なお現在もオープンソースで開発が進められていて、ASP.NET MVC向けのComposition providerなどを含む、MEF 2 Preview 5がCodePlexよりダウンロード可能です。wktkですね。
「MEFとは、簡単に言うとDIである」というような説明がされがちですが、一般的なIoCコンテナ(DIコンテナ)フレームワークほど多機能ではありません。MEFは、 IoCコンテナ(DIコンテナ)で言うところの、いわゆるオブジェクトグラフのファクトリをメインとして機能します。つまり、実行時に解決する必要があるクラスのメンバーを動的に認識して処理することができます。しかし逆に言うと、一般的なIoCコンテナ(DIコンテナ)フレームワークほど豊富な機能はなく、非常にコンパクトな構成です。MEFにもキャッシュ機能があるのは確かですが、インスタンスのキャッシュを最小限しかサポートしません。また、.NET Framework 4 に同梱されているバージョンのMEF では処理のインターセプト(AOPサポート)の機能がまったくありません。純粋に、IoCコンテナ(DIコンテナ)フレームワークのさまざまな機能を求める場合、MEFでは満足できないでしょう。
では、どのような場合にMEFを利用すれば良いのか。それは、アプリケーションが汎用性のある機能拡張(プラグイン,アドイン)を求めているかどうかです。これに該当する場合はMEFの利用が有効で、そうではない場合は、あえてMEFを利用する意味はあまりないでしょう。
拡張(または縮小)可能なクラウドアプリケーション
「拡張(または縮小)可能なクラウドアプリケーション」などと大風呂敷を広げてみたものの、わたしはWindows AzureのWebRoleとWorkerRoleくらいしかかじったことがありません。ここではWindows Azureにおいて、再デプロイを必要としない拡張可能なWebRole、WorkerRoleを作りたいケースについて考えます。.NET Frameworkで拡張可能なアプリケーションを作りたい場合、有効な方法としてMEFがあると前述しました。Azureに置いてもそれは同じです。しかし、Azureで一度デプロイしたものについて動的に機能拡張をするにはどのようにしたらよいのでしょう。まっさきに思い浮かぶのが、Blobストレージの利用です。Blobストレージに格納してあるアセンブリを、ローカルストレージにダウンロードして、DirectoryCatalogクラスを利用してパーツをExportする方法が考えられます。しかしその方法では、対象のアセンブリについてプロセスが掴んでしまうため、デプロイ済みのサービスについて、動的に動作を拡張 or 変更するということができません。ではどうするか。その場合、ローカルストレージは利用せず、BlobストレージからダウンロードしたアセンブリをAssemblyCatalogクラスを利用して直接パーツをExprotする方法を取るとうまくいきます。でも、これをいちいちプログラミングするのは非常に面倒くさいです。常識的に考えて部品化ですね。Blobストレージ内のアセンブリからMEFのパーツを検索できる専用のカタログクラスであるところの、BlobStorageCatalogとか作っちゃえばいいと思います。
以下、実装サンプルです。
BlobStorageCatalog.cs
using System; using System.Collections.Generic; using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition.Primitives; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using Microsoft.WindowsAzure; using Microsoft.WindowsAzure.StorageClient; namespace ClassLibrary2 { public class BlobStorageCatalog : ComposablePartCatalog, ICompositionElement { private readonly object _thisLock = new object(); private readonly CloudStorageAccount _account = null; private readonly static Dictionary<string, byte[]> _dicAssembly = new Dictionary<string, byte[]>(); public string ContainerName { get; private set; } public string BlobName { get; private set; } private AssemblyCatalog _innerCatalog = null; private int _isDisposed = 0; protected BlobStorageCatalog() {} public BlobStorageCatalog(CloudStorageAccount account, string containerName, string blobName) : this() { Contract.Requires(account != null); Contract.Requires(!String.IsNullOrWhiteSpace(containerName)); Contract.Requires(!String.IsNullOrWhiteSpace(blobName)); Contract.Requires(containerName == containerName.ToLower()); this._account = account; this.ContainerName = containerName; this.BlobName = blobName; } private ComposablePartCatalog InnerCatalog { get { this.ThrowIfDisposed(); lock (this._thisLock) { if (_innerCatalog == null) { var catalog = new AssemblyCatalog(LoadAssembly(this._account, this.ContainerName, this.BlobName)); Thread.MemoryBarrier(); this._innerCatalog = catalog; } } return _innerCatalog; } } public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition) { return this.InnerCatalog.GetExports(definition); } public override IQueryable<ComposablePartDefinition> Parts { get { return this.InnerCatalog.Parts; } } private string GetDisplayName() { return string.Format(CultureInfo.CurrentCulture, "{0} (BlobStorage: ContainerName=\"{1}\", BlobName=\"{2}\") (Assembly=\"{3}\")", GetType().Name, this.ContainerName, this.BlobName, _innerCatalog.Assembly.FullName); } public override string ToString() { return GetDisplayName(); } private void ThrowIfDisposed() { if (this._isDisposed == 1) { if (this == null) throw new NullReferenceException(this.GetType().Name); throw new ObjectDisposedException(this.GetType().ToString()); } } protected override void Dispose(bool disposing) { try { if (Interlocked.CompareExchange(ref this._isDisposed, 1, 0) == 0) { if (disposing) { if (this._innerCatalog != null) { this._innerCatalog.Dispose(); } } } } finally { base.Dispose(disposing); } } string ICompositionElement.DisplayName { get { return GetDisplayName(); } } ICompositionElement ICompositionElement.Origin { get { return null; } } private static Assembly LoadAssembly(CloudStorageAccount account, string containerName, string blobname) { var blobStorage = account.CreateCloudBlobClient(); var container = blobStorage.GetContainerReference(containerName); var blob = container.GetBlobReference(blobname); var blobUri = container.Uri + "/" + blobname; using (var strm = new MemoryStream()) { blob.DownloadToStream(strm); byte[] asseblyBytes = strm.ToArray(); if (!_dicAssembly.ContainsKey(blobUri)) { _dicAssembly.Add(blobUri, asseblyBytes); return Assembly.Load(asseblyBytes); } if (Enumerable.SequenceEqual(asseblyBytes, _dicAssembly[blobUri])) { return Assembly.Load(_dicAssembly[blobUri]); } _dicAssembly[blobUri] = asseblyBytes; return Assembly.Load(asseblyBytes); } } } }
だいたいこんな感じですかね。特に問題はないと思いますが、厳密な検証はしていません。実戦投入は計画的に。
拡張(または縮小)可能なWinedow Azure上で動くASP.NET MVC3 Webアプリケーションのサンプル
MEFで拡張可能な計算アプリケーション作成用のインターフェイスを定義
namespace ClassLibrary1 { public interface ICalculator { IEnumerable<char> Symbols { get; } string Calculate(int left, int right, char operation); } public interface IOperation { int Operate(int left, int right); } public interface IOperationData { Char Symbol { get; } } }
足し算と引き算を定義
namespace Calculator { [Export(typeof(IOperation))] [ExportMetadata("Symbol", '-')] class Subtract : IOperation { public int Operate(int left, int right) { return left - right; } } [Export(typeof(IOperation))] [ExportMetadata("Symbol", '+')] class Add : IOperation { public int Operate(int left, int right) { return left + right; } } }
掛け算を新たに定義してビルドし、Calculator.dllを作る
[Export(typeof(IOperation))] [ExportMetadata("Symbol", '*')] public class Multiply : IOperation { public int Operate(int left, int right) { return left * right; } }
ページを再度読み込みなおすと、拡張した掛け算部分が追加されて利用可能に。
という具合に、MEFを利用することで、再度デプロイしなおさなくても、拡張(または縮小)可能なWindows Azureアプリケーションを設計することができます。
このように、MEF は .NET Framework の拡張(または縮小)可能なアプリケーションを構築する際にとても便利なソリューションですが、このとき最も難しい部分は、拡張できるようにアプリケーションを設計することです。これは拡張可能アプリケーション設計そのもの難しさであって、MEFテクノロジそのものが難しいわけではありません。上記のサンプルでは拡張性の乏しい単純な構造の設計となっていますが、より汎用的な拡張が必要なアプリケーションを設計する場合、一気に複雑になります。MEFで提供されているAPIを十分理解した上で、適切な拡張ポイントを見つけ出して、それを将来を見据えたかたちでどれだけ汎用的に拡張できるよう設計できるかがポイントとなってきます。このような設計では、オブジェクト指向の考え方が重要になるでしょう。
ここでは省略しますが、もちろんF#でも同様のアプリケーションを書くことが可能です。
ただF#でMEFを利用する場合はちょっと癖があるんですけどね。でもそれはまた別のお話。
拡張可能なWinedow Azure上で動くASP.NET MVC3 WebアプリケーションのサンプルプログラムをSkyDriveにアップしておきます。
SkyDrive - Azure+MEF.zip