MEFでASP.NETのWeb Formとの連携

下のエントリを踏まえて、MEFを使う場合の連携方法について自分なりの考えを書いておきます。

コンテナの保持

CompositionContainerはシングルトンとして管理します。Webに依存しないようにしておきます。CompositionContainerは、アプリごとに任意のCatalogを使って自由に階層化したはずなので、フレームワークとしては単なるHolderだけを提供します。

    public static class ContainerHolder
    {
        private static readonly Object sync = new object();

        private static CompositionContainer _container;

        public static CompositionContainer Container
        {
            get
            {
                lock (sync)
                {
                    return _container;
                }
            }
            set
            {
                lock (sync)
                {
                    if (_container != null)
                    {
                        throw new InvalidOperationException("Container has already set.");
                    }
                    _container = value;
                }
            }
        }
    }

ちなみに、今のところリクエストスコープとかセッションスコープとか考えていません。なくても困らない気がする。

コンテナの生成

ContainerHolderにCompositionContainerを設定するのはアプリの役目。ASP.NETではGlobal.asaxのApplication_Startメソッドで行うが適切です。

    public class Global : System.Web.HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            var extension = new CompositionContainer(new DirectoryCatalog(HttpRuntime.BinDirectory, "*Extension.dll"));
            var app = new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
            ContainerHolder.Container = new CompositionContainer(extension, app);
        }

        void Application_End(object sender, EventArgs e)
        {
            var container = ContainerHolder.Container;
            if (container != null)
            {
                container.Dispose();
            }
        }
    }

依存注入と自動トランザクション

IHttpModuleでやってもいいのですが、Pageだけに対象を絞るならIHttpHandlerFactoryのほうが適切だと思います。キャストとかいらないし。それと、Pageを処理する直前でトランザクションの自動開始をしたかったというのもあります。

    public class ComposablePageHandlerFactory : PageHandlerFactory
    {
        public override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)
        {
            var handler = base.GetHandler(context, requestType, virtualPath, path);
            if (handler == null)
            {
                return null;
            }
            var container = ContainerHolder.Container;
            if (container != null)
            {
                var part = AttributedModelServices.CreatePart(handler);
                if (part.ImportDefinitions.Any())
                {
                    if (part.ExportDefinitions.Any())
                    {
                        throw new InvalidOperationException("Handlers cannot be exportable");
                    }
                    var batch = new CompositionBatch();
                    batch.AddPart(part);
                    container.Compose(batch);
                }
            }
            return new HandlerWrapper(handler);
        }

        internal class HandlerWrapper : IHttpHandler
        {
            private readonly IHttpHandler _wrapped;

            internal HandlerWrapper(IHttpHandler wrapped)
            {
                _wrapped = wrapped;
            }

            public void ProcessRequest(HttpContext context)
            {
                using(var tx = new TransactionScope())
                {
                    _wrapped.ProcessRequest(context);
                    tx.Complete();
                }
            }

            public bool IsReusable
            {
                get
                {
                    return _wrapped.IsReusable;
                }
            }
        }
    }

いまのところ、Pageから利用されるコントロールには依存注入してません。この機能もなくて困らないような気がしてます。
Page以外のIHttpHandlerに対して依存の注入をしたり、トランザクションを自動で開始したりしたほうがいいのかどうかは今後調査しないとなぁ。

web.configの設定

最後は、web.configの設定。ComposablePageHandlerFactoryを登録します。

  <system.web>
  ...
    <httpHandlers>
      <add verb="*" path="*.aspx" type="WebApplication1.ComposablePageHandlerFactory, WebApplication1"/>
    </httpHandlers>

  </system.web>

これで結構楽にレイヤ分けしたアプリが作れる気がします。Page → Service → Repositoryみたいな。