ASP.NET MVC的插件体系结构


70

我花了一些时间查看Phil Haack关于分组控制器的文章,这些文章非常有趣。

目前,我正在尝试找出是否有可能使用相同的想法为我正在从事的项目创建插件/模块化体系结构。

所以我的问题是:是否可以将Phil文章中的Areas划分到多个项目中?

我可以看到名称空间可以正常工作,但是我担心视图最终出现在正确的位置。是否可以使用构建规则来解决?

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

我有一些插件体系结构方面的经验,但没有大量经验,因此该领域的任何指导都将是有用的。

Answers:


52

几周前,我进行了概念验证,在其中放置了完整的组件堆栈:将模型类,控制器类及其相关视图放入DLL,并添加/调整了其中一个示例了VirtualPathProvider类之一,以便检索视图。他们会适当地解决DLL中的问题。

最后,我只是将DLL放到了正确配置的MVC应用程序中,并且就像它从一开始就已经成为MVC应用程序的一部分一样,它的工作原理也是如此。我将其推得更远,它可以与其中的5个小型mini-MVC插件配合使用。显然,在重新组合它们时,您必须观察自己的引用和配置依赖性,但是它确实起作用。

该练习的目的是针对我为客户端构建的基于MVC的平台的插件功能。在站点的每个实例中,都有一组核心的控制器和视图,并通过更多可选的控件进行了扩充。我们将在这些模块化DLL插件中添加这些可选位。到现在为止还挺好。

我在我的网站上写了我的原型概述和ASP.NET MVC插件示例解决方案

编辑:4年了,我一直在做很多带有插件的ASP.NET MVC应用,并且不再使用我上面描述的方法。在这一点上,我通过MEF运行了所有插件,并且根本不将控制器放入插件中。相反,我制造了使用路由信息选择MEF插件并将工作交给插件等的通用控制器。我以为我会添加,因为这个答案相当合理。


2
您的链接不起作用,我希望看到您的构建,我的项目也遇到类似的问题,我想制作可插拔的项目,以便我可以按需添加/删除功能。与人们在wordpress中的做法相同。
阿洛克

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绑定。

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

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

当您要扩展某些现有功能时,可以创建一个插件,其中包含核心应用程序的子类。当您要创建全新的功能时,可以在插件内部添加新的控制器。当请求相应的URL时,这些控制器将由MVC框架加载。如果要扩展UI,则可以在扩展文件夹中创建一个新视图,并由新的或子类化的控制器引用该视图。要修改现有行为,可以创建新的存储库或域对象或对现有的子类进行子分类。框架的责任是确定应为特定客户加载哪个控制器/存储库/域对象。
我建议看一下结构图(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
这是非常有趣的东西,我喜欢为不同租户重载功能的想法。Ayende的文章很有趣。
西蒙·法罗

4

因此,我在上面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);
    }

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();
        ...
    }
}

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



0

[因为无法发表评论而发布答案]

很好的解决方案-我使用J Wynia的方法,并从单独的程序集中渲染视图。但是,此方法似乎呈现视图。似乎不支持插件中的控制器,对吗?例如,如果某个插件的视图执行了回发操作,则该插件中该视图的控制器不会被调用。相反,它将被路由到根MVC应用程序内的控制器。我是否正确理解了这一点,或者是否有解决此问题的方法?


您可以在本地将路由注册到插件,只需要一些配置它们的方法即可。您可能可以使用StructureMap避免很多反射问题。
Simon Farrow

忽略我的评论/答案。我做错了,但是让它按预期的方式工作。很棒!对不起,噪音!
tbehunin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.