web-dev-qa-db-ja.com

ASP.NET MVCのプラグインアーキテクチャ

Grouping Controllers のPhil Haackの記事を見て、非常に興味深いものを見てきました。

現時点では、同じアイデアを使用して、作業中のプロジェクトのプラグイン/モジュラーアーキテクチャを作成できるかどうかを把握しようとしています。

私の質問は、Philの記事のエリアを複数のプロジェクトに分割することは可能ですか?

名前空間がうまくいくことがわかりますが、ビューが正しい場所に収まるかどうか心配です。ビルドルールで整理できるものですか?

単一のソリューションで複数のプロジェクトを使用して上記が可能であると仮定すると、別のソリューションと定義済みのインターフェイスセットのコーディングでそれを可能にする最良の方法に関するアイデアはありますか?エリアからプラグインへの移動。

プラグインアーキテクチャの経験はありますが、大衆向けではないため、この分野のガイダンスは役立ちます。

70
Simon Farrow

数週間前に概念実証を行って、コンポーネントの完全なスタックを配置しました:モデルクラス、コントローラークラス、およびそれらに関連するビューをDLLに追加し、調整しました 例の1つ ビューを取得するVirtualPathProviderクラスは、DLL内のビューを適切に処理します。

最終的に、適切に構成されたMVCアプリにDLLをドロップしました。最初からMVCアプリの一部であったかのように動作しました。これらの小さなmini-MVCプラグインのうち5つで十分に機能しました。明らかに、参照と設定の依存関係をすべてシャッフルするときに監視する必要がありますが、うまくいきました。

この演習は、私がクライアント向けに構築しているMVCベースのプラットフォームのプラグイン機能を目的としています。コントローラーとビューのコアセットがあり、サイトの各インスタンスにオプションのコントローラーとビューが追加されています。これらのモジュラーDLLプラグインにこれ​​らのオプションビットを作成する予定です。

プロトタイプの概要と ASP.NET MVCプラグインのサンプルソリューション をサイトに書きました。

編集:4年後、私はかなりの数のASP.NET MVCアプリをプラグインで実行し、上記の方法を使用しなくなりました。この時点で、MEFを介してすべてのプラグインを実行し、コントローラーをプラグインにまったく挿入しません。むしろ、ルーティング情報を使用してMEFプラグインを選択し、プラグインなどに作業を渡す汎用コントローラーを作成します。この答えがかなりヒットするので追加したいと思っただけです。

51
J Wynia

実際に、ASP.NET MVCの上で使用するための拡張性フレームワークに取り組んでいます。私の拡張性フレームワークは、有名なIocコンテナであるStructuremapに基づいています。

私が実現しようとしているユースケースは簡単です。すべての顧客(=マルチテナンシー)に拡張できる基本的な機能を備えたアプリケーションを作成します。ホストされるアプリケーションのインスタンスは1つのみである必要がありますが、このインスタンスは、コアWebサイトに変更を加えることなく、すべての顧客に適合させることができます。

Ayende Rahienによって書かれたマルチテナシーに関する記事に触発されました: http://ayende.com/Blog/archive/2008/08/16/Multi-Tenancy--Approaches-and-Applicability.aspx =別のインスピレーションの源は、ドメイン駆動設計に関するエリックエヴァンスの本でした。私の拡張性フレームワークは、リポジトリパターンとルートアグリゲートの概念に基づいています。フレームワークを使用できるようにするには、リポジトリとドメインオブジェクトを中心にホスティングアプリケーションを構築する必要があります。コントローラ、リポジトリ、またはドメインオブジェクトは、ExtensionFactoryによって実行時にバインドされます。

プラグインは、特定の命名規則を尊重するコントローラーまたはリポジトリーまたはドメインオブジェクトを含む単なる集合体です。命名規則は単純です。すべてのクラスには、customerIDをプレフィックスとして付ける必要があります(例:AdventureworksHomeController)。

アプリケーションを拡張するには、アプリケーションの拡張フォルダーにプラグインアセンブリをコピーします。ユーザーが顧客のルートフォルダーの下のページをリクエストする場合: http://multitenant-site.com/ [customerID]/[controller]/[action] プラグインがあるかどうかのフレームワークチェック-その特定の顧客向けにカスタムプラグインクラスをインスタンス化します。そうでない場合は、デフォルトを1回ロードします。カスタムクラスは、コントローラ–リポジトリまたはドメインオブジェクトです。このアプローチにより、データベースからUI、ドメインモデル、リポジトリを介して、すべてのレベルでアプリケーションを拡張できます。

既存の機能の一部を拡張する場合は、コアアプリケーションのサブクラスを含むアセンブリのプラグインを作成します。まったく新しい機能を作成する必要がある場合は、プラグイン内に新しいコントローラーを追加します。これらのコントローラーは、対応するURLが要求されると、MVCフレームワークによってロードされます。 UIを拡張する場合、拡張フォルダー内に新しいビューを作成し、新しいまたはサブクラス化されたコントローラーによってビューを参照できます。既存の動作を変更するには、新しいリポジトリまたはドメインオブジェクトを作成するか、既存のサブクラスを作成します。フレームワークの責任は、特定の顧客に対してどのコントローラー/リポジトリ/ドメインオブジェクトをロードするかを決定することです。
structuremap( http://structuremap.sourceforge.net/Default.htm )、特にRegistry DSL機能をご覧になることをお勧めします http:// structuremap.sourceforge.net/RegistryDSL.htm

これは、アプリケーションの起動時にすべてのプラグインコントローラー/リポジトリまたはドメインオブジェクトを登録するために使用するコードです。

protected void ScanControllersAndRepositoriesFromPath(string path)
        {
            this.Scan(o =>
            {
                o.AssembliesFromPath(path);
                o.AddAllTypesOf<SaasController>().NameBy(type => type.Name.Replace("Controller", ""));
                o.AddAllTypesOf<IRepository>().NameBy(type => type.Name.Replace("Repository", ""));
                o.AddAllTypesOf<IDomainFactory>().NameBy(type => type.Name.Replace("DomainFactory", ""));
            });
        }

System.Web.MVCを継承するExtensionFactoryも使用します。 DefaultControllerFactory。このファクトリーは、拡張オブジェクト(コントローラー/レジストリーまたはドメインオブジェクト)をロードします。 Global.asaxファイルに起動時に登録することにより、独自のファクトリをプラグインできます。

protected void Application_Start()
        {
            ControllerBuilder.Current.SetControllerFactory(
                new ExtensionControllerFactory()
                );
        }

完全に動作するサンプルサイトとしてのこのフレームワークは、次の場所にあります。 http://code.google.com/p/multimvc /

14
Geo

そのため、上記の J Wynia の例を少し試してみました。そのことに感謝します。

VirtualPathProviderの拡張機能が静的コンストラクターを使用して、システム内のさまざまなdllで.aspxで終わるすべての利用可能なリソースのリストを作成するように変更しました。それは骨の折れる作業ですが、私たちだけが一度だけやっています。

これはおそらくVirtualFilesが同様に使用されることになっている方法の完全な乱用です;-)

あなたは最終的に:

プライベートスタティックIDictionary resourceVirtualFile;

文字列は仮想パスです。

以下のコードは、.aspxファイルの名前空間についていくつかの仮定を行いますが、単純な場合には機能します。この素晴らしい点は、リソース名から作成される複雑なビューパスを作成する必要がないことです。

class ResourceVirtualFile : VirtualFile
{
    string path;
    string assemblyName;
    string resourceName;

    public ResourceVirtualFile(
        string virtualPath,
        string AssemblyName,
        string ResourceName)
        : base(virtualPath)
    {
        path = VirtualPathUtility.ToAppRelative(virtualPath);
        assemblyName = AssemblyName;
        resourceName = ResourceName;
    }

    public override Stream Open()
    {
        assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName + ".dll");

        Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyName);
        if (Assembly != null)
        {
            Stream resourceStream = Assembly.GetManifestResourceStream(resourceName);
            if (resourceStream == null)
                throw new ArgumentException("Cannot find resource: " + resourceName);
            return resourceStream;
        }
        throw new ArgumentException("Cannot find Assembly: " + assemblyName);
    }

    //todo: Neaten this up
    private static string CreateVirtualPath(string AssemblyName, string ResourceName)
    {
        string path = ResourceName.Substring(AssemblyName.Length);
        path = path.Replace(".aspx", "").Replace(".", "/");
        return string.Format("~{0}.aspx", path);
    }

    public static IDictionary<string, VirtualFile> FindAllResources()
    {
        Dictionary<string, VirtualFile> files = new Dictionary<string, VirtualFile>();

        //list all of the bin files
        string[] assemblyFilePaths = Directory.GetFiles(HttpRuntime.BinDirectory, "*.dll");
        foreach (string assemblyFilePath in assemblyFilePaths)
        {
            string assemblyName = Path.GetFileNameWithoutExtension(assemblyFilePath);
            Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFilePath);  

            //go through each one and get all of the resources that end in aspx
            string[] resourceNames = Assembly.GetManifestResourceNames();

            foreach (string resourceName in resourceNames)
            {
                if (resourceName.EndsWith(".aspx"))
                {
                    string virtualPath = CreateVirtualPath(assemblyName, resourceName);
                    files.Add(virtualPath, new ResourceVirtualFile(virtualPath, assemblyName, resourceName));
                }
            }
        }

        return files;
    }
}

その後、拡張VirtualPathProviderで次のようなことを実行できます。

    private bool IsExtended(string virtualPath)
    {
        String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
        return resourceVirtualFile.ContainsKey(checkPath);
    }

    public override bool FileExists(string virtualPath)
    {
        return (IsExtended(virtualPath) || base.FileExists(virtualPath));
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        string withTilda = string.Format("~{0}", virtualPath);

        if (resourceVirtualFile.ContainsKey(withTilda))
            return resourceVirtualFile[withTilda];

        return base.GetFile(virtualPath);
    }
4
Simon Farrow

プラグインプロジェクトにビューを残すことは可能だと思います。

それが私の考えです:プラグインを(おそらくインターフェイスを介して)呼び出し、ビュー(IView)を要求するViewEngineが必要です。プラグインは、URLを使用して(通常のViewEngineのように-Views/Shared/View.aspを使用)、ビューの名前を使用してビューをインスタンス化します(リフレクションまたはDI/IoCコンテナーを使用)。

プラグインでビューを返すと、ハードコーディングされることさえあります(簡単な例を次に示します)。

public IView GetView(string viewName)
{
    switch (viewName)
    {
        case "Namespace.View1":
            return new View1();
        case "Namespace.View2":
            return new View2();
        ...
    }
}

...これは単なるアイデアでしたが、うまくいくか、良いインスピレーションになることを願っています。

3
gius

この投稿は少し遅れるかもしれませんが、私はASP.NET MVC2で遊んでおり、「エリア」機能を使用したプロトタイプを思いついています。

ここに興味がある人のためのリンクがあります: http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/

3
Veebs

[コメントできないため回答として投稿]

素晴らしい解決策-J Wyniaによるアプローチを使用して、別のアセンブリからビューをレンダリングするようにした。ただし、このアプローチはonlyのように見えます。プラグイン内のコントローラーはサポートされていないようです、正しいですか?たとえば、プラグインからのビューがポストバックを行った場合、プラグイン内のそのビューのコントローラーwill not be called。代わりに、コントローラにルーティングされますルートMVCアプリケーション内。これを正しく理解していますか、またはこの問題の回避策はありますか?

0
tbehunin