新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序(续1)

  今天在@张善友@田园里的蟋蟀的博客看到微软“.Net社区虚拟大会”dotnetConf2015的信息,感谢他们的真诚付出!真希望自已也能为中国的.NET社区贡献绵薄之力。


  上周星期天开通了博客并发布了第一篇文章《新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序》,汇集了一些比较流行的技术和开源项目,也把自己的程序架构、部分代码风格、前端表现简单做了一些展示,引起了近100位朋友的评论。特别感谢@田园里的蟋蟀、@深蓝医生、@郭明锋、@疯狂的提子、@jimcsharp、@以吾之名等给我建议和指导的朋友,也感谢那些给我支持和鼓励的朋友。还有对我提出批评的朋友,说我的面试题的内容不当,也很感谢他们让我更注意言辞,但并不会影响我对面试者基础知识的重视程度。


  上周发布那篇文章主要是因为这段时间在招聘过程中发现几乎所有面试者对基础知识和新技术都知之甚少,有过几年工作经验的程序员也几乎只会单一模式的CURD,没有明显的技术特长,所以我想分享一些自己认为比较好的思想、技术、架构模式,引起更多ASP.NET程序员的思考和讨论。


其实,上周星期天是花了大半天写一篇博客,在发出来之前删掉了一大半内容(一些讲述我自己心路历程的内容),因为我在博客园是一个新人,在没有对别人提供价值帮助之前也许没人关心我是谁。那天由于时间太晚了,很多想写的内容都没有写出来,发布的时候仅贴了一些图片,后来在评论中写了很多内容,并修改了原文正文,补充分享了一些非常好的开源项目。希望之前看过的朋友可以再回去看看,给个链接:http://www.cnblogs.com/mienreal/p/4340864.html


  之前的一个项目是做的微信公众平台的第三方平台,提供微网站自主建站、会员卡、微商城、外卖预订等几十项功能。在项目初期,我仅担任产品总监负责产品设计,后来因为没有强大的前端团队,不得不亲自实现微官网的可视化设计器的前端。再后来公司让我接管了开发部(全是JAVA开发人员),跟开发团队有了更直接的配合。我发现他们普遍代码质量不高,几乎不懂得运用设计模式和最佳实践。每新增或修改一点功能,都要将全部代码进行编译和发布,会影响正在登录使用的用户,而且有时候一个经验不足的程序员修改的一点东西会让整个平台不能正常启动。跟几个高级工程师多次沟通,希望他们学习新技术新思想,运用成熟的最佳实践来提高代码质量;希望他们了解领域驱动设计用于会员卡等业务较复杂的模块;希望他们能了解OSGI实现模块化开发和部署,但因为经验能力和积极性等原因,这些愿望都没有实现。后来在新项目(开发代号Fami)中,我选择了.NET技术平台,并组建新的开发团队来进行这个项目。现在项目才刚完成基础框架和项目规范。


  下面把这个项目的架构思想和功能特性再分享一下。希望对正在设计架构的朋友有一个参考作用。本项目是Saas模式的在线产品,需实现多租户模式;有多个功能模块,且上线时间有先有后,需实现模块化开发。

 

本项目总体分为两个部分:一个基础框架组件,一个Fami解决方案。

 技术分享

 

基础框架组件的功能:
1、基础框架组件独立、通用,可用于多个不同项目。类似于daxnet的Apworks框架。
2、对项目实现模块化开发提供了支持,每个模块有独立的EF DbContext,可单独指定数据库。
3、对DDD的技术实现进行了封装,让项目以极精简的代码,专注于业务领域。
4、多租户支持,每个租户的数据自动隔离,业务模块开发者不需要手动操作TenantId。
5、集成ASP.NET Identity,实现登录认证、功能权限授权&验证、角色和用户管理。
6、集成Log4Net,实现日志记录。
7、集成AutoMapper,实现Dto类与实体类的双向自动转换。
8、实现UnitOfWork模式,为应用层和仓储层的(会写数据库的)方法自动实现数据库事务。
9、可通过ApplicationService的方法自动建立相应的WebApi方法,ajax可直接调用,不需要写ApiController和Action。
10、调用ApplicationService的方法时,自动验证权限和参数有效性(用相应的Attribute标注)。
11、实现一系列扩展方法,简化编码。

 

Fami项目解决方案结构图:

技术分享   技术分享
模块化结构图   WEB项目结构图

 

每个模块是一个独立的类库项目,有独立的DbContext(如上面左图中的WechatMpDbContext.cs),可单独指定不同的数据库链接,以实现按功能模块分库。

每个模块有自己权限提供类(WechatMpAuthorizationProvider.cs)、设置提供类(WechatMpSettingProvider.cs)、仓储基类(WechatMpRepository.cs)。

模块的展现层代码(MVC文件)放在WEB项目的Areas下,有自己单独的路由注册类文件(如上面右图中的WechatMpAreaRegistration.cs)。

 

MVC的Controller只有极少的代码,用于返回列表页的View、表单页面的View和Model,新建、编辑、删除等操作无需写Action方法,直接由前端的ajax调用Application层的相应Service方法(运行时,动态代理自动生成ApiController及相应方法)。

拿一个最最简单的图文素材功能举例说明:

 

Domain层的Article实体类:

 1 namespace Fami.WechatMp
 2 {
 3     public class Article : AuditedEntityAndTenant
 4     {
 5         [MaxLength(50)]
 6         public string Title { get; set; }
 7 
 8         [MaxLength(512)]
 9         public string PicUrl { get; set; }
10 
11         [MaxLength(1000)]
12         public string Interoduction { get; set; }
13 
14         [MaxLength(512)]
15         public string LinkUrl { get; set; }
16 
17         [MaxLength(512)]
18         public string OriginalUrl { get; set; }
19 
20         public string Content { get; set; }
21 
22         [ForeignKey("ArticleCategoryId")]
23         public ArticleCategory ArticleCategory { get; set; }
24 
25         public Guid ArticleCategoryId { get; set; }
26     }
27 }

 

Application层的ArticleDto类(用于WEB前端表单与Application层之间传值):

 1 namespace Fami.WechatMp
 2 {
 3     [AutoMap(typeof(Article))]
 4     public class ArticleDto : EntityDto, IValidate
 5     {
 6         [Required]
 7         [MaxLength(50)]
 8         public string Title { get; set; }
 9 
10         [MaxLength(512)]
11         public string PicUrl { get; set; }
12 
13         [MaxLength(1000)]
14         public string Interoduction { get; set; }
15 
16         [MaxLength(512)]
17         public string LinkUrl { get; set; }
18 
19         [MaxLength(512)]
20         public string OriginalUrl { get; set; }
21 
22         public string Content { get; set; }
23 
24         public Guid ArticleCategoryId { get; set; }
25     }
26 }

 

Application层的ArticleItem类(用于WEB前端查询列表的显示):

 1 namespace Fami.WechatMp
 2 {
 3     [AutoMapFrom(typeof(Article))]
 4     public class ArticleItem : EntityDto
 5     {
 6         public string Title { get; set; }
 7 
 8         public string PicUrl { get; set; }
 9 
10         public string LinkUrl { get; set; }
11 
12         public string OriginalUrl { get; set; }
13 
14         public string ArticleCategoryCategoryName { get; set; } //会自动读取ArticleCategory的CategoryName属性
15 
16         public DateTime CreationTime { get; set; }
17     }
18 }

 

Application层的IArticleAppService接口:

 1 namespace Fami.WechatMp
 2 {
 3     public interface IArticleAppService : IApplicationService
 4     {
 5         /// <summary>
 6         /// 获取素材分类列表(下拉框)
 7         /// </summary>
 8         /// <returns></returns>
 9         Task<IEnumerable<ArticleCategoryDto>> GetArticleCategories();
10 
11         #region 素材查询和更新操作
12         /// <summary>
13         /// 创建素材信息
14         /// </summary>
15         /// <param name="model"></param>
16         /// <returns></returns>
17         Task<ArticleDto> CreateArticle(ArticleDto model);
18 
19         /// <summary>
20         /// 更新素材信息
21         /// </summary>
22         /// <param name="model"></param>
23         /// <returns></returns>
24         Task UpdateArticle(ArticleDto model);
25 
26         /// <summary>
27         /// 批量删除素材信息
28         /// </summary>
29         /// <param name="input"></param>
30         /// <returns></returns>
31         Task BatchDeleteArticle(IEnumerable<Guid> idList);
32 
33         /// <summary>
34         /// 获取指定的素材信息
35         /// </summary>
36         /// <param name="id"></param>
37         /// <returns></returns>
38         Task<ArticleDto> GetArticle(Guid id);
39 
40         /// <summary>
41         /// 查询素材列表信息(Table)
42         /// </summary>
43         /// <param name="input"></param>
44         /// <returns></returns>
45         Task<QueryResultOutput<ArticleItem>> GetArticleList(GetArticleListInput input);
46 
47         #endregion
48     }
49 }

 

Application层的ArticleAppService实现类:

 1 namespace Fami.WechatMp
 2 {
 3     public class ArticleAppService : FamiAppServiceBase, IArticleAppService
 4     {
 5         private readonly IWechatMpRepository<ArticleCategory> _articleCategoryRepository;
 6         private readonly IWechatMpRepository<Article> _articleRepository;
 7         private readonly IArticlePolicy _articlePolicy;
 8 
 9         public ArticleAppService(
10             IWechatMpRepository<ArticleCategory> articleCategoryRepository,
11             IWechatMpRepository<Article> articleRepository,
12             IArticlePolicy articlePolicy
13             )
14         {
15             _articleCategoryRepository = articleCategoryRepository;
16             _articleRepository = articleRepository;
17             _articlePolicy = articlePolicy;
18         }
19 
20         public async Task<IEnumerable<ArticleCategoryDto>> GetArticleCategories()
21         {
22             var query = _articleCategoryRepository.GetAll().OrderBy(item => item.DisplayOrder);
23             return await query.Query().To<ArticleCategoryDto>().Take(100).ToListAsync();
24         }
25 
26         public async Task<ArticleDto> CreateArticle(ArticleDto model)
27         {
28             if (await _articlePolicy.IsExistsArticleByName(model.Title))
29             {
30                 throw new UserFriendlyException(L("NameIsExists"));
31             }
32             var entity = await _articleRepository.InsertAsync(model.MapTo<Article>());
33             return entity.MapTo<ArticleDto>();
34         }
35 
36         public async Task UpdateArticle(ArticleDto model)
37         {
38             if (await _articlePolicy.IsExistsArticleByName(model.Title, model.Id))
39             {
40                 throw new UserFriendlyException(L("NameIsExists"));
41             }
42             var entity = await _articleRepository.GetAsync(model.Id);
43             await _articleRepository.UpdateAsync(model.MapTo(entity));
44         }
45 
46         public async Task BatchDeleteArticle(IEnumerable<Guid> idList)
47         {
48             if (await _articlePolicy.IsExistsByArticleAutoreplySetting(idList.ToList()))
49             {
50                 throw new UserFriendlyException(L("AutoreplyArticleIsExists"));
51             }
52             await _articleRepository.BatchDeleteAsync(idList);
53         }
54 
55         public async Task<ArticleDto> GetArticle(Guid id)
56         {
57             var entity = await _articleRepository.GetAsync(id);
58             return entity.MapTo<ArticleDto>();
59         }
60 
61         /// <summary>
62         /// 根据查询条件,返回文章列表数据
63         /// </summary>
64         /// <param name="input">查询条件</param>
65         /// <returns></returns>
66         public async Task<QueryResultOutput<ArticleItem>> GetArticleList(GetArticleListInput input)
67         {
68             var query = _articleRepository.GetAll()
69                 .WhereIf(input.ArticleCategoryId.HasValue, m => m.ArticleCategoryId == input.ArticleCategoryId.Value)
70                 .WhereIf(!input.Keywords.IsNullOrWhiteSpace(), m => m.Title.Contains(input.Keywords));
71 
72             var result = await query.Query(input).ToAsync<ArticleItem>();
73             return result;
74         }
75     }
76 }

 

ArticleController.cs代码如下:

 1 namespace Fami.Mc.Web.Controllers
 2 {
 3     public class ArticleController : FamiControllerBase
 4     {
 5         private readonly IArticleAppService _articleAppService;
 6 
 7         public ArticleController(IArticleAppService articleAppService)
 8         {
 9             _articleAppService = articleAppService;
10         }
11 
12         public async Task<ActionResult> Index()
13         {
14             ViewBag.ArticleCategoryDtos = await _articleAppService.GetArticleCategories();
15             return View();
16         }
17 
18         public async Task<ActionResult> Edit(Guid? id)
19         {
20             ArticleDto model;
21             if (!id.HasValue)  //新建
22             {
23                 model = new ArticleDto();
24                 ViewBag.ActionName = "createArticle";
25             }
26             else  //编辑
27             {
28                 model = await _articleAppService.GetArticle(id.Value);
29                 ViewBag.ActionName = "updateArticle";
30             }
31             ViewBag.ArticleCategoryDtos = await _articleAppService.GetArticleCategories();
32             return View(model);
33         }
34     }
35 }

 

Views/Article/Index.cshtml代码(列表页):

 1 <div class="page-content">
 2     <div class="page-header">
 3         <div class="page-title">文章管理</div>
 4         <!-- 过滤条件start -->
 5         <div id="filterbar" class="alert alert-lightsGray fs12 clearfix">
 6             <div class="clearfix" style="margin-right:30px;">
 7                 <div class="clearfix pull-left" style="line-height: 30px; margin: 3px 5px; ">
 8                     <div class="pull-left">分类:</div>
 9                     <div class="pull-left">
10                         @Html.DropDownList("ArticleCategoryId", new SelectList(ViewBag.ArticleCategoryDtos, "Id", "CategoryName"), "", new { @class = "form-control w180"})
11                     </div>
12                 </div>
13                 <div class="clearfix pull-left" style="line-height: 30px; margin: 3px 5px;">
14                     <div class="pull-left">搜索:</div>
15                     <div class="input-group input-group-sm w130">
16                         <input class="form-control pull-left" placeholder="文章标题" filterfield="Keywords" name="Keywords" type="text">
17                         <span class="input-group-btn">
18                             <button class="btn btn-default btnSearch" type="button"><i class="icon-search2 fs14"></i></button>
19                         </span>
20                     </div>
21                 </div>
22             </div>
23         </div>
24         <!-- 过滤条件end -->
25     </div>
26 
27     <!-- 列表上的功能按钮放在这里 -->
28     <div class="buttons-panel">
29         <button id="btnNew" class="btn btn-primary"><i class="icon-plus2"></i>新增文章</button>
30         <button id="btnEdit" class="btn btn-default"><i class="icon-edit"></i>编辑</button>
31         <button id="btnDeletes" class="btn btn-default"><i class="icon-trash"></i>删除 </button>
32         <button id="btnReload" class="btn btn-default"><i class="icon-refresh"></i>刷新 </button>
33     </div>
34     <table id="mytable" class="wx-listview table table-bordered"></table>
35 </div>
36 @section js{
37     @Scripts.Render("~/js/datatables")
38     <script src="~/Areas/WechatMp/js/article.js"></script>
39 }

 

article.js代码:

 1 var listColumns = [
 2         listCheckboxColumn,
 3         { "name": "id", "data": "id", title: "ID", "sortable": false, "visible": false },
 4         { "name": "title", "data": "title", title: "名称" },
 5         {
 6             "name": "picUrl", "data": "picUrl", title: "图片", "width": "100", "sortable": false,
 7             "render": function (data) { return ‘<img src="‘ + abp.resourcePath + data + ‘" style="width:60px;"/>‘;}
 8         },
 9         { "name": "articleCategoryCategoryName", "data": "articleCategoryCategoryName", title: "所属分类" },
10         { "name": "linkUrl", "data": "linkUrl", title: "外链地址" },
11         { "name": "originalUrl", "data": "originalUrl", title: "原文地址" },
12         { "name": "creationTime", "data": "creationTime", title: "创建时间", "width": "180" }
13 ];
14 
15 $(function () {
16     abp.grid.init({
17             order: [[abp.grid.getColIndex("creationTime"), "desc"]],
18             filterbar: "#filterbar",//过滤区域selector
19             table: "#mytable",//table selector
20             ajax: abp.grid.ajaxLoadEx({
21                 "url": abp.appPath + "api/wechatmp/article/getArticleList",
22             }),
23             columns: listColumns
24         });
25 
26     //新增
27     $("#btnNew").click(function () {
28         abp.dialog({
29             width: "900px",
30             title: "新增文章",
31             href: abp.appPath + ‘WechatMp/Article/Edit‘,
32             callback: abp.grid.reloadList
33         });
34     });
35 
36     //编辑
37     $("#btnEdit").on(‘click‘, function () {
38         var row = abp.grid.getSelectedOneRowData();
39         if (!row) return;
40         abp.dialog({
41             width: "900px",
42             title: "编辑分类",
43             href: abp.appPath + ‘WechatMp/Article/Edit/‘ + row.id,
44             callback: abp.grid.reloadList
45         });
46     });
47 
48     //删除
49     $("#btnDeletes").on(‘click‘, function () {
50         var idList = abp.grid.getSelectedIdList();
51         if (idList.length == 0) return;
52 
53         abp.confirm(abp.utils.formatString("您确认要删除选中的{0}行吗?", idList.length), function (result) {
54             if (!result) return; //取消
55             abp.ajax({
56                 url: abp.appPath + ‘api/wechatmp/article/batchDeleteArticle‘,
57                 data: idList
58             }).done(function (ret) {
59                 abp.success("删除成功");
60                 abp.grid.reloadList();
61             });
62         });
63     });
64 })

 

界面截图:

技术分享

 

由于这个功能实在太简单,没有使用到领域服务、领域事件,这里可能只能说明一件事件:没有复杂业务逻辑的功能使用此DDD框架,并不会增加代码量,反而我认为这样的代码量差不多已经少到极致了。

 

真没想到今晚又搞到这么晚,一篇文章写了5个小时了,写文章实在太慢了!有兴趣的朋友还是互动讨论吧。

 

以后再对框架的每一种机制进行详细说明。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。