基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现

基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现

概述

  在日常开发中, 我们经常谈起模块化/插件化架构,这样可既可以提高开效率,又可以实现良好的扩展性,尤其对于产品化的系统有更好的实用性。

架构

  我们采用的是MVC5(本文中介绍的方法对于MVC4也是适用的),如下图,解决方案中有四个项目,其中 WeDiscuss 为前端,WeDiscuss.Plugin.Framework 为插件公共类库  WeDiscuss.Plugin.Album 为插件(相册) WeDiscuss.Plugin.News 为插件(新闻),本文只是讲解决插件的实现方式,就不多做其它如果业务逻辑、数据访问层等

  注;每个插件都有自已的(M、V、C),内部实现和常用MVC没有区别,这样可以方便的开发,没有其它新知识的引入。

  基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现_第1张图片

  其中,插件层可以在主项目中引用,也可以不引用,或是放到其它目录下(如把插件DLL单独放到“Plugins”目录中),如果不引用就采用在编译完成时复制

  下面讲解编译完成复制方法,如想复制到“Plugins”目录中请修改BIN为“Plugins”:

  在如下图加入:

copy /Y "$(TargetDir)$(ProjectName).dll" "$(SolutionDir)Wediscuss\Bin\"

基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现_第2张图片

如何让ASP.NET加载BIN目录之外的路径的Assembly

  我们把各个模块编译出来的assembly和各个模块的配置文件自动放到一个bin平级的plugin目录,然后web应用启动的时候自动扫描这个plugin目录并加载各个模块plugin,这个怎么做到的?大家也许知道,ASP.NET只允许读取Bin目录下的assbmely,不可以读取其他路径,包括Bin\abc等,即使在web.config这样配置probing也不行:(不信你可以试一下)

 

复制代码
 <configuration> Element
  <runtime> Element
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
       <probing privatePath="bin;plugins;"/>
     </assemblyBinding>
    </runtime>
</configuration>
复制代码

如何注入菜单

  插件能用了,但也想动态注入菜单,这样才实现了自动化,要不还是人工进行菜单注入永远是半自动化,这和我们开发的思想是不想符的,下面就来说一下菜单的注入

  1、首称在WeDiscuss.Plugin.Framework 为插件公共类库中建实体类PluginMenu 和PluginMenus

复制代码
/*
 * -------------------------------------------------------------------------------
 * 功能描述:
 * 
 * 创建人:     JunHan(俊涵)
 * 创建日期:    2013/12/15 21:59:16
 * 创建说明:    
 * 
 * 修改人:     
 * 修改日期:    
 * 修改说明:    
 *    
 * -------------------------------------------------------------------------------
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace WeDiscuss.Plugin.Framework
{
    public class PluginMenus
    {
        public List<PluginMenu> MenuList { get; set; }

        public string CssClass { get; set; }

        public int MenuType { get; set; }

        public string Html
        {
            get
            {
                StringBuilder stringBuilder = new StringBuilder();
                foreach (var menu in MenuList)
                {
                    TagBuilder tagBuilder = new TagBuilder("a");
                    tagBuilder.MergeAttribute("href", menu.MenuUrl);
                    tagBuilder.InnerHtml = menu.MenuText;
                    tagBuilder.MergeAttribute("class", CssClass);
                    stringBuilder.Append(tagBuilder.ToString(TagRenderMode.Normal) + "\r\n");
                }
                return stringBuilder.ToString();
            }
        }

        public List<PluginMenu> AvailableList
        {
            get
            {
                if (MenuList == null)
                {
                    return new List<PluginMenu>();
                }
                if (MenuType == 0)
                {
                    return MenuList;
                }
                if (!MenuList.Any(o => o.MenuType == MenuType))
                {
                    return new List<PluginMenu>();
                }
                return MenuList.Where(o => o.MenuType == MenuType).ToList();
            }
        }
    }

    public class PluginMenu
    {
        public string MenuText { get; set; }
        public string MenuUrl { get; set; }

        public int MenuType { get; set; }

        public int MenuOrder { get; set; }

        public bool Visible { get; set; }
    }
}
复制代码

  这样我们就实现了菜单的结构,接下来就是采单的生成或注入方法:

  新建 AppPlugin 和 PluginApplication来实现菜单的初使化方法,并将生成好的菜单存放在静态变量中。

复制代码
/*
 * -------------------------------------------------------------------------------
 * 功能描述:
 * 
 * 创建人:     JunHan(俊涵)
 * 创建日期:    2013/12/15 23:29:58
 * 创建说明:    
 * 
 * 修改人:     
 * 修改日期:    
 * 修改说明:    
 *    
 * -------------------------------------------------------------------------------
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WeDiscuss.Plugin.Framework
{
    public class PluginApplication : BaseMvcPluginApplication
    {
        #region Plugin Menu Support
        public static PluginMenus PluginMenus = new PluginMenus();
        public void RegisterMenuItem(PluginMenu menu)
        {
            lock (PluginMenus)
            {
                if (PluginMenus.MenuList == null) PluginMenus.MenuList = new List<PluginMenu>();
                PluginMenus.MenuList.Add(new PluginMenu() { MenuText = menu.MenuText, MenuUrl = menu.MenuUrl, MenuType = menu.MenuType, MenuOrder = menu.MenuOrder });
                PluginMenus.MenuList = PluginMenus.MenuList.OrderBy(o => o.MenuOrder).ToList();
            }
        }
        #endregion

        public static new PluginApplication Instance
        {
            get { return BaseMvcPluginApplication.Instance as PluginApplication; }
            set { BaseMvcPluginApplication.Instance = value; }
        }


        protected override bool ShouldIncludeResourceCore(BaseMvcPluginApplication.ResourceTypes type, IMvcPlugin plugin)
        {
            return ShouldIncludeResource(plugin, null);
        }

        protected virtual bool ShouldIncludeResource(IMvcPlugin plugin, object resource)
        {
            bool should = true;
            if (plugin != null)
            {
                if ((should = plugin.Enabled) && plugin is AppPlugin)
                    should = ((AppPlugin)plugin).ShouldIncludeResource(resource);
            }

            return should;
        }

        protected override void AddAdditionalRazorViewLocationsCore(List<string> lst)
        {
            lst.Add("~/Plugins/PluginDemo/Views.{1}.{0}.cshtml");
        }


        public static PluginApplication SetupApplication(object bundles, object routes)
        {
            PluginApplication me = new PluginApplication(bundles, routes);
            return me;
        }

        protected PluginApplication(object bundles, object routes)
            : base(bundles, routes)
        {
        }
    }

    public class AppPlugin : BaseMvcPlugin
    {
        public PluginApplication _App { get { return (PluginApplication)App; } }

        public AppPlugin(bool ensureStandardViewLocation = true)
            : base(ensureStandardViewLocation)
        {
        }

        public void DefineMenuItem(PluginMenu item)
        {
            _App.RegisterMenuItem(item);
        }

        public virtual bool ShouldIncludeResource(object content)
        {
            return true;
        }
    }
}
复制代码

  2、菜单初使化

  在每个插件项目中新建一类,并继承AppPlugin,重写方法:SetupExtensions 调用DefineMenuItem 实现菜单初使化,在菜单的结果中我们看到有MenuType类型,这里我们自定义,一般会用枚举来实现,可以定义为前台或后台等,一个插件可以拥有多个菜单,可以注入多个地方

  

复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WeDiscuss.Plugin.Framework;

namespace WeDiscuss.Plugin.Album
{
    [Export(typeof(IMvcPlugin))]
    [MvcPluginMetadata("AlbumPlugin", null, "Demo App Site Album", "")]
    class AlbumPlugin : AppPlugin
    {
        public override void SetupExtensions(IMvcPluginApplication app)
        {
            base.SetupExtensions(app);
            DefineMenuItem(new PluginMenu { MenuText = "相册", MenuUrl = "/Album", MenuType = 1, MenuOrder = 1 });
            DefineMenuItem(new PluginMenu { MenuText = "相册管理", MenuUrl = "/ManageAlbum", MenuType = 2, MenuOrder = 1 });
        }
    }
}
复制代码

  3、菜单调用

    在需要出现插件菜单的地方,我们用以下方法实现菜单的注入并呈现

  

复制代码
@using WeDiscuss.Plugin.Framework
@{
    var menu = PluginApplication.PluginMenus;
    menu.MenuType = 1;
}
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - 我的 ASP.NET 应用程序</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")

</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("应用程序名称", "Index", "Home", null, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("主页", "Index", "Home")</li>
                    <li>@Html.ActionLink("关于", "About", "Home")</li>
                    <li>@Html.ActionLink("联系方式", "Contact", "Home")</li>
                    @{
                        foreach (var item in menu.AvailableList)
                        {
                            <li><a href="@item.MenuUrl">@item.MenuText</a></li>
                        }
                    }
                </ul>
                @Html.Partial("_LoginPartial")
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - 我的 ASP.NET 应用程序</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>
复制代码


小结

  本文主要讲解了实现方法,并没有讲内部结构是如何实现的,以后的分享中我们会慢慢讲解内部实现逻辑和产品化中的应用和配置。大这有问题或是好的建议想法可以发邮件给我 [email protected] 如果您修改了代码以实现更好的功能,也烦请转发我一份谢谢!

源码下载

  我们站在前辈的肩膀上成长,感谢所有帮助WD成长的人.

  源码中没有加入packages,请大家自行加载,源码下载 ,没有密码全部开放!

 

asp.net mvc route 中新发现的小技巧

在发现这个小技巧之前,我经常被某些问题困扰,我们以博客园为例

基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现_第3张图片

1:是分类名称

2:是分类url

3:点击分类,进入的页面,要显示分类的名称

4:点击分类,进入的页面,要用分类相关参数

在日常web的开发中,经常遇到要在页面上显示一个分类,然后这个分类的链接要包含这个分类的id等参数(例如为搜索引擎友好要包含名称的汉语拼音),然后点击这个分类的页面要显示这个分类的各种信息例如分类名称,分类读取搜索接口的地址和参数等。

尤其是某些变态的分类,一个条目和一个条目读取相关信息的方式还不一样,还可能很耗时

这个时候为了提升系统性能,经常用缓存办法,尤其是整体高并发站点(不是某些热数据造成的并发,而是网站整体的并发比较大),更是头疼

我们按照web开发无限细分来看这个问题

1 直接读取数据 ==》  数据库连接数被占用

2 缓存到asp.net自带的cache  ==》我经常偷懒用这个,数据库连接数被占用的次数减少但不明显,因为缓存的时间短(缓存长了貌似没用)

3缓存到分布式缓存例如Memcache  ==》序列化和反序列化造成cpu上升和内网流量上升

3缓存到分布式缓存例如Memcache+本地cache缓存等  ==》方案较为复杂

直到最近写route ,手抖了一下,以为自己写错了热烈的笑脸

假设 博客园的分类是这么定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public  class  CnblogsCate
   {
       public  int  id { get ; set ; }
       public  string  name { get ; set ; }
       public  string  search { get ; set ; }
 
       /*
       其他相关的属性
        *
       */
 
       public  List<CnblogsCate> getALL()
       {
           List<CnblogsCate> result = new  List<CnblogsCate>();
           CnblogsCate item = new  CnblogsCate();
           item.id = 1;
           item.name = "test" ;
           item.search = "pra=1" ;
           result.Add(item);
           item = new  CnblogsCate();
           item.id = 2;
           item.name = "test2" ;
           item.search = "pra=2" ;
           result.Add(item);
           return  result;
           //return new List<CnblogsCate>
       }
 
   }

注册route

1
2
3
4
5
6
7
8
9
10
       CnblogsCate  Cate= new  CnblogsCate();
       var  allCate= Cate.getALL();
       foreach  (CnblogsCate item in  allCate)
       {
           routes.MapRoute(
     "Default_"  + item.id, // 路由名称
     "Cnblog/" +item.name+ "/" , // 带有参数的 URL
     new  { controller = "Cnblogs" , action = "Index" , Cate = item } // 这里直接对参数赋值
);
       }

这样在控制器里面就可以直接取到想要的数据了

1
2
3
4
5
6
7
8
9
10
11
12
public  class  CnblogsController : Controller
{
     //
     // GET: /Cnblogs/
 
     public  ActionResult Index(CnblogsCate Cate)
     {
 
         return  View();
     }
 
}
1
  

例如

请求:http://localhost:34197/Cnblog/test/

基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现_第4张图片

这样就可以和各种复杂的方案说拜拜了,即使是动态添加和删除分类对应的rout也很简单,和复杂方案说886

test
 
分类:  asp.net mvc

AspNet WebApi OData 学习

OData介绍:是一个查询和更新数据的Web协议。OData应用了web技术如HTTP、Atom发布协议(AtomPub)和JSON等来提供对不同应用程序,服务 和存储的信息访问。除了提供一些基本的操作(像增删改查),也提供了一些高级的操作类似过滤数据和实体的导航。OData扩展了上述的协议但是不是取代他 们。他可以被XML(ATOM)或者JSON取代但是OData的重要在于它符合REST原则。在某种意义上,它建立在'简单'的REST HTTP 服务上,并且有着清晰的目标——简化和标准化我们操作和查询数据的方式。如果你过去在给你的REST服务创建搜索、过滤、或者分页API的时候感觉很麻 烦,那么OData将是一个不错的选择。

 

OData好处:通过OData,我们采取不同的方法。取代创建客户端签名和参数,我们问了如下的问题:“如果你将数据集作为源处理,并为最频繁使用的操作定义模式,像查 询、分页、排序、新建、删除和更新,服务接口因该是什么样子的?” 这也就导致OData的创建。OData解决了上面提到的关键服务设计挑战。

          

由于Visual Studio 2012里面的 AspNet WebApi 2 OData   中的 AspNet WebApi Client 5.0 依赖于 .Net Framework 4.5 框架,

所以只能去下载,.Net Framework 4.0 版本的 AspNet WebApi OData

AspNet WebApi OData 版本下载地址:http://www.nuget.org/packages/Microsoft.AspNet.WebApi.OData.zh-Hans/4.0.30506

程序包管理器,去安装:

PM> Install-Package Microsoft.AspNet.WebApi.OData -Version 4.0.30506

  $filter 用法:

Return all products with category equal to “Toys”. http://localhost/Products?$filter=Category eq 'Toys'
Return all products with price less than 10. http://localhost/Products?$filter=Price lt 10
Logical operators: Return all products where price >= 5 and price <= 15. http://localhost/Products?$filter=Price ge 5 and Price le 15
String functions: Return all products with “zz” in the name. http://localhost/Products?$filter=substringof('zz',Name)
Date functions: Return all products with ReleaseDate after 2005. http://localhost/Products?$filter=year(ReleaseDate) gt 2005

 

$orderby 用法:

Sort by price. http://localhost/Products?$orderby=Price
Sort by price in descending order (highest to lowest). http://localhost/Products?$orderby=Price desc
Sort by category, then sort by price in descending order within categories. http://localhost/odata/Products?$orderby=Category,Price desc

PageSize  用法:

复制代码
[Queryable(PageSize=10)] public IQueryable<Product> Get() { return products.AsQueryable(); }  客户端点击链接筛选到下一页,为了让客户端得到页码,我们必须求出结果集,这个客户端可以使用$inlinecount的一个叫“allpages”的参数,来得到总条数。
http:
//localhost/Products?$inlinecount=allpages 这个“allpages”值,就是服务端包含结果集的总数,响应告诉给客户端: { "odata.metadata":"http://localhost/$metadata#Products", "odata.count":"50", "value":[ { "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" }, { "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" }, ] }
//此方法,是用来处理分页数据查询,过滤,我们可以很方便灵活的去在客户端做处理。
public PageResult<Product> Get(ODataQueryOptions<Product> options) { ODataQuerySettings settings = new ODataQuerySettings() { PageSize = 5 }; IQueryable results = options.ApplyTo(_products.AsQueryable(), settings); return new PageResult<Product>( results as IEnumerable<Product>, Request.GetNextPageLink(), Request.GetInlineCount()); }

这里是一个返回Json的例子:

{

  "Items"[{"ID":1,"Name":"Hat","Price":"15","Category":"Apparel"},

 {"ID":2,"Name":"Socks","Price":"5","Category":"Apparel"},// Others not shown],
"NextPageLink" :"http://localhost/api/values?$inlinecount=allpages&$skip=10",
"Count": 50
}
 
复制代码

 

博客参考学习地址:

http://www.cnblogs.com/cube/p/3484115.html

http://www.cnblogs.com/shanyou/archive/2013/06/11/3131583.html

http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options

在寂寞的日子里沉淀自己,在程序的日子里找到自己,我为梦想而坚持!

  接口控制类模型描述用户接口与系统其他层之间的通信,接口控制类位于系统结构的商业上下文服务层,接口控制类模型用类图和包图描述。首先简要介绍接口控制类模型的设计方法,然后设计子系统的接口控制类与接口类的类图,最后设计系统及子系统的接口控制类的包图。

  1、设计方法

  接口控制类承担用户接口与应用程序的其他层之间通信的大多数工作,接口控制类比较简单,对于每一个需要与应用程序的其他层进行通信的用户接口,都应该有一个相应的接口控制类,对应的一个接口类即定义一个接口控制类。接口控制类通常是临时的,不用保存在外部存储器中,其生命周期在交互完成时结束。为使类之间的交互简捷、明了,接口控制类只与接口类、用例控制类有关系,接口控制类之间不应该有关系,接口类依赖接口控制类,而接口控制类又依赖用例控制类。接口控制类的操作和属性可以在设计序列图时定义,也可以在实现阶段设计程序代码时定义。

  2、类图

  前一篇文章“软件工程之系统建模篇【设计接口类模型】”已经定义了发文办理接口类,这里我们依然以前一篇文章的发文办理流程来定义接口控制类,我们分别对接口类定义接口控制类,但是不直接与应用程序的其他层通信的类,则不需要定义接口控制类。由于接口控制类之间不存在关系,类图应该反映接口控制类和接口类之间的关系。发文办理接口控制类包括:UICSFileList、UICDraftSFile、UICAuditSFileList、UICAuditSFile、UICCheckSFileList、UICCheckSFile、UICSignSFileList、UICSignSFile、UICEnreSFileList、UICEnreSFile、UICSendSFile,下图为发文办理接口控制类和接口类的类图,在图中区别有两个类型的类,接口控制类的名称用斜体字显示。

  基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现_第5张图片

  3、包图

  我们将系统实例的接口控制类从系统层划分为8个包,分别为发文办理接口控制类包UICSFilePack、收文办理接口控制类包UICRFilePack、会议管理接口控制类包UICMeetingPack、档案管理接口控制类包UICArchivePack、公告管理接口控制类包UICNoticePack、个人助理接口控制类包UICAssistantPack、系统管理接口控制类包UICSSystemPack、用户登录接口控制类UICLogin。

  基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现_第6张图片

  我们将发文办理子系统的接口控制类,进一步划分为6个包:草拟发文接口控制类包UICDraftSFilePack、审核发文接口控制类包UICAuditSFilePack、复核发文接口控制类包UICCheckSFilePack、签发发文接口控制类包UICSignSFilePack、分发发文接口控制类包UICEnreSFilePack和送发文至档案室接口控制类包UICSendSFilePack。

  基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现_第7张图片

  至此发文办理流程中的每一个用户接口,都对应一个接口控制类,下一章我们将介绍设计用例控制类模型。

  

 
 
分类:  软件工程

 

 
分类:  WD开发
标签:  Plugin,  MVC插件,  MVC5插件,  MVC4插件,  MVC模块化

你可能感兴趣的:(基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现)