1. ホーム
  2. c#

MEF with MVC 4 or 5 - プラガブル・アーキテクチャ (2014)

2023-09-04 20:53:57

質問

Orchard CMSのようなプラグイン可能なアーキテクチャを持つMVC4/MVC5アプリケーションを構築しようとしています。だから、私はスタートアップのプロジェクトであるMVCアプリケーションを持っていて、認証、ナビゲーションなどの世話をする。その後、複数のモジュールが asp.net クラスライブラリまたはストリップダウンされた MVC プロジェクトとして別々に構築され、コントローラ、ビュー、データリポジトリなどを持つことになります。

私は一日中 Web 上のチュートリアルを調べ、サンプルなどをダウンロードしましたが、Kenny が最も良い例を持っていることがわかりました。 http://kennytordeur.blogspot.in/2012/08/mef-in-aspnet-mvc-4-and-webapi.html

私は、モジュール (別の DLL) に参照を追加すれば、それらの DLL からコントローラをインポートすることができます。しかし、MEF を使用する理由は、実行時にモジュールを追加することができることです。私はビューと一緒にDLLをスタートアッププロジェクトの~/Modules//ディレクトリにコピーして(私はこれを行うことができました)、MEFがそれらを拾うようにしたいのです。MEFにこれらのライブラリをロードさせるために苦労しています。

この回答で説明されているように、MefContrib もあります。 ASP.NET MVC 4.0 コントローラーとMEF、この2つを一緒にする方法は? というのがあり、これは私が次に試そうとしているものです。しかし、私はMEFがMVCで箱から出して動かないことに驚いています。

誰か同じようなアーキテクチャを(MefContribの有無にかかわらず)動作させたことがありますか?当初は、Orchard CMS を取り外してフレームワークとして使用することも考えましたが、それはあまりにも複雑です。また、WebAPI2 を利用するために、MVC5 でアプリを開発するのもいいでしょう。

どのように解決するのですか?

私は、あなたが説明したようなプラグイン可能なアーキテクチャを持つプロジェクトで働いたことがあり、それは同じ技術ASP.NET MVCとMEFを使用しました。認証、承認、およびすべての要求を処理する、ホスト ASP.NET MVC アプリケーションがありました。私たちのプラグイン(モジュール)はそのサブフォルダーにコピーされました。プラグインは、独自のモデル、コントローラ、ビュー、CSS、JSファイルを持っているASP.NET MVCアプリケーションでした。これらは、私たちがそれを動作させるために従った手順です。

MEF をセットアップする

アプリケーション開始時にすべてのコンポーザブルパーツを発見し、コンポーザブルパーツのカタログを作成する MEF ベースのエンジンを作成しました。これは、アプリケーションの開始時に 1 回だけ実行されるタスクです。エンジンはすべてのプラグイン可能な部品を検出する必要があり、私たちの場合は bin フォルダーに、あるいは Modules(Plugins) フォルダーに保存されます。

public class Bootstrapper
{
    private static CompositionContainer CompositionContainer;
    private static bool IsLoaded = false;

    public static void Compose(List<string> pluginFolders)
    {
        if (IsLoaded) return;

        var catalog = new AggregateCatalog();

        catalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")));

        foreach (var plugin in pluginFolders)
        {
            var directoryCatalog = new DirectoryCatalog(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", plugin));
            catalog.Catalogs.Add(directoryCatalog);

        }
        CompositionContainer = new CompositionContainer(catalog);

        CompositionContainer.ComposeParts();
        IsLoaded = true;
    }

    public static T GetInstance<T>(string contractName = null)
    {
        var type = default(T);
        if (CompositionContainer == null) return type;

        if (!string.IsNullOrWhiteSpace(contractName))
            type = CompositionContainer.GetExportedValue<T>(contractName);
        else
            type = CompositionContainer.GetExportedValue<T>();

        return type;
    }
}

これは、すべての MEF パーツの検出を行うクラスのサンプルコードです。このクラスは Compose メソッドから呼び出されます。 Application_Start メソッドから呼び出されます。 Global.asax.cs ファイルを作成します。簡略化のため、コードを縮小しています。

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        var pluginFolders = new List<string>();

        var plugins = Directory.GetDirectories(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules")).ToList();

        plugins.ForEach(s =>
        {
            var di = new DirectoryInfo(s);
            pluginFolders.Add(di.Name);
        });

        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        Bootstrapper.Compose(pluginFolders);
        ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
        ViewEngines.Engines.Add(new CustomViewEngine(pluginFolders));
    }
}

すべてのプラグインが Modules フォルダーの別のサブフォルダーにコピーされることを想定しています。各プラグインのサブフォルダーには Views サブフォルダーと各プラグインのDLLが含まれています。このうち Application_Start メソッドでは、カスタムコントローラファクトリーと、これから定義するカスタムビューエンジンも初期化されています。

MEFから読み込むコントローラファクトリの作成

以下は、リクエストを処理する必要があるコントローラを検出する、カスタムコントローラファクトリを定義するコードです。

public class CustomControllerFactory : IControllerFactory
{
    private readonly DefaultControllerFactory _defaultControllerFactory;

    public CustomControllerFactory()
    {
        _defaultControllerFactory = new DefaultControllerFactory();
    }

    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller = Bootstrapper.GetInstance<IController>(controllerName);

        if (controller == null)
            throw new Exception("Controller not found!");

        return controller;
    }

    public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
    {
        return SessionStateBehavior.Default;
    }

    public void ReleaseController(IController controller)
    {
        var disposableController = controller as IDisposable;

        if (disposableController != null)
        {
            disposableController.Dispose();
        }
    }
}

さらに、各コントローラは Export 属性が必要です。

[Export("Plugin1", typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class Plugin1Controller : Controller
{
    //
    // GET: /Plugin1/
    public ActionResult Index()
    {
        return View();
    }
}

の最初のパラメータは Export 属性コンストラクタの最初のパラメータは、コントラクト名を指定し、各コントローラを一意に識別するため、一意でなければなりません。そのため PartCreationPolicy は NonShared に設定する必要があります。なぜなら、コントローラは複数のリクエストに再利用できないからです。

プラグインからビューを見つけることができるビューエンジンを作成する

カスタムビューエンジンの作成が必要なのは、ビューエンジンが慣習的に Views フォルダーにあるビューだけを探すためです。プラグインは別のフォルダにあるため、カスタムビューエンジンを作成する必要があります。 Modules フォルダーに格納されているため、ビューエンジンにそこも参照するように指示する必要があります。

public class CustomViewEngine : RazorViewEngine
{
    private List<string> _plugins = new List<string>();

    public CustomViewEngine(List<string> pluginFolders)
    {
        _plugins = pluginFolders;

        ViewLocationFormats = GetViewLocations();
        MasterLocationFormats = GetMasterLocations();
        PartialViewLocationFormats = GetViewLocations();
    }

    public string[] GetViewLocations()
    {
        var views = new List<string>();
        views.Add("~/Views/{1}/{0}.cshtml");

        _plugins.ForEach(plugin =>
            views.Add("~/Modules/" + plugin + "/Views/{1}/{0}.cshtml")
        );
        return views.ToArray();
    }

    public string[] GetMasterLocations()
    {
        var masterPages = new List<string>();

        masterPages.Add("~/Views/Shared/{0}.cshtml");

        _plugins.ForEach(plugin =>
            masterPages.Add("~/Modules/" + plugin + "/Views/Shared/{0}.cshtml")
        );

        return masterPages.ToArray();
    }
}

プラグインで強く型付けされたビューの問題を解決する

上記のコードだけでは、プラグイン(モジュール)内で強く型付けされたビューを使うことができませんでした。なぜなら、モデルは bin フォルダの外にモデルが存在するためです。この問題を解決するために、以下のようにします。 リンク .