ASP.NET MVC的插件体系结构

时间:2008-12-04 10:40:22

标签: asp.net-mvc plugins

我花了一些时间看看Phil Haack关于Grouping Controllers非常有趣的文章。

目前我正在试图弄清楚是否可以使用相同的想法为我正在开发的项目创建插件/模块化架构。

所以我的问题是:Phil的文章中的区域是否可能分为多个项目?

我可以看到名称空间会自行解决,但我担心最终会出现在正确的位置。是否可以使用构建规则进行整理?

假设在单个解决方案中有多个项目可以实现上述目标,那么是否有人对使用单独的解决方案和编码到预定义的接口集的最佳方法有任何想法?从区域移动到插件。

我有一些插件架构的经验,但不是群众,所以这方面的任何指导都会有用。

6 个答案:

答案 0 :(得分:51)

几周前,我做了一个概念验证,在那里我放了一个完整的堆栈组件:一个模型类,一个控制器类及其相关的DLL视图,添加/调整了VirtualPathProvider类的one of the examples检索视图,以便他们适当地解决DLL中的问题。

最后,我只是将DLL放入一个经过适当配置的MVC应用程序中,就像它从一开始就是MVC应用程序的一部分一样。我推了一下它,它与5个这样的小迷你MVC插件一起使用就好了。显然,你必须在整理它时观察你的引用和配置依赖,但它确实有效。

该练习针对的是我正在为客户构建的基于MVC的平台的插件功能。有一组核心控制器和视图,在站点的每个实例中都有更多可选的控制器和视图。我们将把这些可选位转换为这些模块化DLL插件。到目前为止一切都很好。

我在我的网站上写了我的原型概述和sample solution for ASP.NET MVC plugins

编辑:4年后,我一直在使用插件做很多ASP.NET MVC应用程序,不再使用我上面描述的方法。此时,我通过MEF运行所有插件,并且根本不将控制器放入插件中。相反,我制作了使用路由信息来选择MEF插件并将工作交给插件等的通用控制器。只是想我会添加,因为这个答案会受到一些影响。

答案 1 :(得分:14)

我实际上正在开发一个可扩展性框架,以便在ASP.NET MVC之上使用。我的可扩展性框架基于着名的Ioc容器:Structuremap。

我想要实现的用例很简单:创建一个应该具有一些基本功能的应用程序,可以为每个客户扩展(=多租户)。应该只托管一个应用程序实例,但可以为每个客户调整此实例,而无需对核心网站进行任何更改。

我受到Ayende Rahien写的关于多重性的文章的启发:http://ayende.com/Blog/archive/2008/08/16/Multi-Tenancy--Approaches-and-Applicability.aspx 另一个灵感来源是Eric Evans关于领域驱动设计的书。 My Extensibility框架基于存储库模式和根聚合的概念。为了能够使用该框架,托管应用程序应该围绕存储库和域对象构建。控制器,存储库或域对象在运行时由ExtensionFactory绑定。

插件只是一个包含控制器或存储库或域对象的组件,它们遵循特定的命名约定。命名约定很简单,每个类都应该以customerID为前缀,例如:AdventureworksHomeController。

要扩展应用程序,请将插件程序集复制到应用程序的扩展文件夹中。当用户请求客户根文件夹下的页面时,例如:  http://multitenant-site.com/[customerID]/[controller]/[action] 框架检查是否有该特定客户的插件并实例化自定义插件类,否则它会加载默认值一次。自定义类可以是控制器 - 存储库或域对象。此方法可以在各个级别(从数据库到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 /

答案 2 :(得分:4)

所以我对上面的J Wynia示例进行了一些讨论。非常感谢btw。

我更改了一些内容,以便VirtualPathProvider的扩展使用静态构造函数来创建系统中各个dll中以.aspx结尾的所有可用资源的列表。这很费力但只有我们只做一次。

这可能完全滥用了VirtualFiles的使用方式; - )

你最终得到了:

private static 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);
    }

答案 3 :(得分:3)

我想可以将您的观点留在插件项目中。

这就是我的想法:你需要一个可以调用插件的ViewEngine(可能通过一个接口)并请求视图(IView)。然后,插件将不通过其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();
        ...
    }
}

......这只是一个想法,但我希望它可以起作用或者只是一个很好的灵感。

答案 4 :(得分:3)

这篇文章可能有点晚了,但我一直在玩ASP.NET MVC2并使用“区域”功能提出原型。

以下是感兴趣的人的链接:http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/

答案 5 :(得分:0)

[张贴作为答案,因为我无法发表评论]

很好的解决方案 - 我使用了J Wynia的方法并让它从一个单独的程序集中呈现视图。但是,此方法似乎呈现视图。插件中的控制器似乎不受支持,对吗?例如,如果插件中的视图发回了帖子,那么插件中的视图控制器将称为。相反,它将被路由到根MVC应用程序中的控制器。我是否正确理解这个问题,或者是否有针对此问题的解决方法?