ASP.NET MVC的插件架构

70

我花了一些时间阅读Phil Haack的文章《在ASP.NET MVC中分组控制器》,内容非常有趣。

目前,我正在尝试弄清楚是否可以使用相同的思路为我正在进行的项目创建插件/模块化体系结构。

因此,我的问题是:是否可能将Phil的文章中的区域(Areas)分割成多个项目?

我可以看到名称空间会自己解决,但我担心视图最终不在正确的位置。这是可以通过构建规则解决的吗?

假设在单个解决方案中可以使用上述方法处理多个项目,那么有没有人对如何以单独的解决方案和编码到预定义接口集的方式实现这一点有任何想法?从区域转移到插件。

我有一些插件架构方面的经验,但并不是非常丰富,因此在这方面的任何指导都将是有用的。

4个回答

52
我几周前进行了一个概念验证,将完整的组件堆栈:模型类、控制器类及其相关视图放入DLL中,并添加/调整VirtualPathProvider类的其中一个示例以检索视图,以便适当地处理DLL中的视图。最后,我只需将DLL放入配置正确的MVC应用程序中,它就像从一开始就是MVC应用程序的一部分一样工作。我进一步推动了它,并且使用了5个这些小型的迷你MVC插件,效果很好。显然,在移动所有内容时,您必须注意引用和配置依赖项,但它确实有效。这个练习旨在为我正在为客户构建的基于MVC的平台提供插件功能。有一组核心控制器和视图,每个站点实例都可以增加更多可选的控制器和视图。我们将把这些可选元素制作成这些模块化的DLL插件。目前为止一切顺利。我在我的网站上写了一个概述我的原型和ASP.NET MVC插件的示例解决方案

编辑:四年后,我已经使用了相当多的带有插件的ASP.NET MVC应用程序,并且不再使用我上面描述的方法。此时,我通过MEF运行所有插件,根本不将控制器放入插件中。相反,我制作通用控制器,使用路由信息选择MEF插件并将工作交给插件等。只是想添加一下,因为这个答案被访问得相当多。


2
你的链接不起作用,我希望能够看到你所构建的东西。我的项目也有类似的问题,我想创建可插拔式项目,以便可以按需添加/删除功能,就像WordPress中的人们所做的一样。 - Alok

14

我正在开发一个 ASP.NET MVC 扩展框架。这个框架基于著名的 IoC 容器 Structuremap。

我的用例很简单:创建一个应用程序,该应用程序应具有一些基本功能,可以为每个客户定制(多租户)。应该只有一个实例托管在服务器上,但可以针对每个客户进行适应,而不需要更改核心网站。

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

插件只是包含符合特定命名约定的控制器、存储库或域对象的程序集。命名约定很简单,每个类都应以客户 ID 作前缀,例如: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.DefaultControllerFactory的ExtensionFactory。这个工厂负责加载扩展对象(控制器/注册表或领域对象)。你可以通过在Global.asax文件中启动时注册它们来插入自己的工厂:

protected void Application_Start()
        {
            ControllerBuilder.Current.SetControllerFactory(
                new ExtensionControllerFactory()
                );
        }
该框架的完整操作示例网站可以在以下链接找到:http://code.google.com/p/multimvc/。请注意,该网站为英文页面。

2
这是非常有趣的东西,我喜欢为不同租户重载功能的想法。Ayende的文章很有意思。 - Simon Farrow

4

我对来自J Wynia的示例进行了一些尝试。非常感谢。

我更改了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
我猜在插件项目中留下你的观点是可能的。
我的想法是:你需要一个ViewEngine,它将通过接口调用插件并请求视图(IView)。然后,插件将不会像普通的ViewEngine那样通过其url(如/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();
        ...
    }
}

这只是一个想法,但我希望它能够实现或成为一个很好的灵感。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接