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

  上周星期天开通了博客并发布了第一篇文章《新思想、新技术、新架构——更好更快的开发现代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、继承自FullAuditedEntity基类的领域实体,会自动实现软删除(在数据库中用IsDeleted字段进行标注)。
12、实现一系列扩展方法,简化编码。

 

 

 

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.WechatMp2 {3     public class Article : AuditedEntityAndTenant4     {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.WechatMp2 {3     [AutoMap(typeof(Article))]4     public class ArticleDto : EntityDto, IValidate5     {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.WechatMp2 {3     [AutoMapFrom(typeof(Article))]4     public class ArticleItem : EntityDto5     {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.WechatMp2 {3     public interface IArticleAppService : IApplicationService4     {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.WechatMp2 {3     public class ArticleAppService : FamiAppServiceBase, IArticleAppService4     {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.Controllers2 {3     public class ArticleController : FamiControllerBase4     {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 })

复制代码

 

界面截图:

 

在进行这个列表查询时,客户端ajax直接调用ArticleAppService的GetArticleList方法,看下浏览器请求:

会根据文章分类的下拉选项,自动生成ArticleCategoryId的查询过滤参数。

 

 

服务端执行GetArticleList方法,自动把客户端ajax提交的数据组装成input参数(GetArticleListInput类指定的结构),然后根据过滤条件进行查询:

复制代码

 1         /// <summary>2         /// 根据查询条件,返回文章列表数据3         /// </summary>4         /// <param name="input">查询条件</param>5         /// <returns></returns>6         public async Task<QueryResultOutput<ArticleItem>> GetArticleList(GetArticleListInput input)7         {8             var query = _articleRepository.GetAll()9                 .WhereIf(input.ArticleCategoryId.HasValue, m => m.ArticleCategoryId == input.ArticleCategoryId.Value)
10                 .WhereIf(!input.Keywords.IsNullOrWhiteSpace(), m => m.Title.Contains(input.Keywords));
11 
12             var result = await query.Query(input).ToAsync<ArticleItem>();
13             return result;
14         }

复制代码

 这个例子中仅过滤了ArticleCategoryId,没有输入标题中的关键字

 

EF自动生成的SQL如下,只查ArticleItem类指定的字段,会自动关键文章分类表查取分类名称,会自动根据当前登录用户的TenantId(租户Id)来过滤。

并且取总记录数和取指定页数据的两步操作,仅会生成一条Sql语句在SqlServer中执行:

 

复制代码

 1 exec sp_executesql N'-- Query #12 3 SELECT 4     [GroupBy1].[A1] AS [C1]5     FROM ( SELECT 6         COUNT(1) AS [A1]7         FROM [dbo].[WechatMp_Article] AS [Extent1]8         WHERE (cast(''e5f2aea7-1423-4708-8162-7d029f5966d1'' as uniqueidentifier) = [Extent1].[TenantId]) AND ([Extent1].[ArticleCategoryId] = @f0_p__linq__0)9     )  AS [GroupBy1];
10 
11 -- Query #2
12 
13 SELECT TOP (10) 
14     [Project1].[C1] AS [C1], 
15     [Project1].[Title] AS [Title], 
16     [Project1].[PicUrl] AS [PicUrl], 
17     [Project1].[LinkUrl] AS [LinkUrl], 
18     [Project1].[OriginalUrl] AS [OriginalUrl], 
19     [Project1].[CategoryName] AS [CategoryName], 
20     [Project1].[CreationTime] AS [CreationTime], 
21     [Project1].[Id] AS [Id]
22     FROM ( SELECT 
23         [Extent1].[Id] AS [Id], 
24         [Extent1].[Title] AS [Title], 
25         [Extent1].[PicUrl] AS [PicUrl], 
26         [Extent1].[LinkUrl] AS [LinkUrl], 
27         [Extent1].[OriginalUrl] AS [OriginalUrl], 
28         [Extent1].[CreationTime] AS [CreationTime], 
29         [Extent2].[CategoryName] AS [CategoryName], 
30         1 AS [C1]
31         FROM  [dbo].[WechatMp_Article] AS [Extent1]
32         INNER JOIN [dbo].[WechatMp_ArticleCategory] AS [Extent2] ON [Extent1].[ArticleCategoryId] = [Extent2].[Id]
33         WHERE (cast(''e5f2aea7-1423-4708-8162-7d029f5966d1'' as uniqueidentifier) = [Extent1].[TenantId]) AND ([Extent1].[ArticleCategoryId] = @f1_p__linq__0)
34     )  AS [Project1]
35     ORDER BY [Project1].[CreationTime] DESC;
36 ',N'@f0_p__linq__0 uniqueidentifier,@f1_p__linq__0 uniqueidentifier',@f0_p__linq__0='05506DBD-A0CB-449D-82F9-A462014C4440',@f1_p__linq__0='05506DBD-A0CB-449D-82F9-A462014C4440'

复制代码

 

 

  

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

 

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

 

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

 

—————————————————————————————————————————————————————————————— 

2015-3-23 13:10补充:

下面贴一下框架层Repository基类的接口,为了显示简洁,我发到这里的代码把注释全去掉了,从方法名称和参数很容易知道他们的作用,

除返回IQueryable<TEntity>接口的GetAll()方法,其他都有同步和异步两个版本。

复制代码

  1 public interface IRepository<TEntity, TPrimaryKey> : IRepository where TEntity : class, IEntity<TPrimaryKey>2 {3     IQueryable<TEntity> GetAll();4 5     List<TEntity> GetAllList();6 7     Task<List<TEntity>> GetAllListAsync();8 9     List<TEntity> GetAllList(Expression<Func<TEntity, bool>> predicate);10 11     Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate);12 13     TEntity Get(TPrimaryKey id);14 15     Task<TEntity> GetAsync(TPrimaryKey id);16 17     TEntity Single(Expression<Func<TEntity, bool>> predicate);18 19     Task<TEntity> SingleAsync(Expression<Func<TEntity, bool>> predicate);20 21     TEntity FirstOrDefault(TPrimaryKey id);22 23     Task<TEntity> FirstOrDefaultAsync(TPrimaryKey id);24 25     TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate);26 27     Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);28 29     TEntity Insert(TEntity entity);30 31     Task<TEntity> InsertAsync(TEntity entity);32 33     TPrimaryKey InsertAndGetId(TEntity entity);34 35     Task<TPrimaryKey> InsertAndGetIdAsync(TEntity entity);36 37     TEntity InsertOrUpdate(TEntity entity);38 39     Task<TEntity> InsertOrUpdateAsync(TEntity entity);40 41     TPrimaryKey InsertOrUpdateAndGetId(TEntity entity);42 43     Task<TPrimaryKey> InsertOrUpdateAndGetIdAsync(TEntity entity);44 45     TEntity Update(TEntity entity);46 47     Task<TEntity> UpdateAsync(TEntity entity);48 49     TEntity Update(TPrimaryKey id, Action<TEntity> updateAction);50 51     Task<TEntity> UpdateAsync(TPrimaryKey id, Func<TEntity, Task> updateAction);52 53     int BatchUpdate(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TEntity>> updateExpression);54 55     Task<int> BatchUpdateAsync(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TEntity>> updateExpression);56 57     void BatchUpdateDisplayOrder(IEnumerable<TPrimaryKey> idList);  58 59     Task BatchUpdateDisplayOrderAsync(IEnumerable<TPrimaryKey> idList);60 61     void Delete(TEntity entity);62 63     Task DeleteAsync(TEntity entity);64 65     void Delete(TPrimaryKey id);66 67     Task DeleteAsync(TPrimaryKey id);68 69     void Delete(Expression<Func<TEntity, bool>> predicate);70 71     Task DeleteAsync(Expression<Func<TEntity, bool>> predicate);72 73     void Delete(IEnumerable<TPrimaryKey> idList);74 75     Task DeleteAsync(IEnumerable<TPrimaryKey> idList);76 77     void BatchDelete(Expression<Func<TEntity, bool>> predicate);78 79     Task BatchDeleteAsync(Expression<Func<TEntity, bool>> predicate);80 81     void BatchDelete(IEnumerable<TPrimaryKey> idList);82 83     Task BatchDeleteAsync(IEnumerable<TPrimaryKey> idList);84 85     int Count();86 87     Task<int> CountAsync();88 89     int Count(Expression<Func<TEntity, bool>> predicate);90 91     Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate);92 93     long LongCount();94 95     Task<long> LongCountAsync();96 97     long LongCount(Expression<Func<TEntity, bool>> predicate);98 99     Task<long> LongCountAsync(Expression<Func<TEntity, bool>> predicate);
100 }

复制代码

 

可能只有BatchUpdateDisplayOrder方法可能不太容易理解,我单独说明一下:这个是列表页面对表格行手动上下拖动排序后,根据idList传入的Id及顺序,更新DisplayOrder字段

(只有在数据量不大,不需要分页的情况下,才允许使用这种方式手动排序)

 

 

 

--------------------------------------------------------------------------------------------------

2015-3-23 15:40补充 回复@何镇汐 多租户机制的自动实现

自动实现两方面的操作:

1、新建实体时自动从当前用户的session中取出所属的租户标识(TenantId) 给实体的TenantId赋值

2、查询数据时自动根据当前用户的TenantId过滤

 

先说第1个,自动赋值的实现方式:

拿本文上面的创建文章例子来说明

ArticleAppService的CreateArticle方式主要代码如下:

        public async Task<ArticleDto> CreateArticle(ArticleDto model){var entity = await _articleRepository.InsertAsync(model.MapTo<Article>());return entity.MapTo<ArticleDto>();}

CreateArticle方法中“model.MapTo<Article>()” 会自动创建Article实体类的实例(在基类的构造函数中自动生成Guid类型的Id),并将表单控件输入的值(Dto类的属性)赋值给新建的实体类,然后调用仓储基类的Insert方法,这时并没有提交到数据库。因为框架会自动给CreateArticle方法应用UnitOfWork并开启数据库事务,当CreateArticle方法顺利执行完毕(没有抛出异常),会应用框架基类DbContext中的SaveChangesAsync方法,做一些自动赋值和事件触发后再调用base.SaveChangesAsync

请看代码:

复制代码

 1         public override int SaveChanges()2         {3             ApplyAbpConcepts();4             return base.SaveChanges();5         }6 7         public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)8         {9             ApplyAbpConcepts();
10             return base.SaveChangesAsync(cancellationToken);
11         }
12 
13         private void ApplyAbpConcepts()
14         {
15             foreach (var entry in ChangeTracker.Entries())
16             {
17                 switch (entry.State)
18                 {
19                     case EntityState.Added:
20                         SetCreationAuditProperties(entry);
21                         EntityEventHelper.TriggerEntityCreatingEvent(entry.Entity);  // <-- 请看这里
22                         EntityEventHelper.TriggerEntityCreatedEvent(entry.Entity);
23                         break;
24                     case EntityState.Modified:
25                         if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
26                         {
27                             HandleSoftDelete(entry);
28                             EntityEventHelper.TriggerEntityDeletedEvent(entry.Entity);
29                         }
30                         else
31                         {
32                             SetModificationAuditProperties(entry);
33                             EntityEventHelper.TriggerEntityUpdatedEvent(entry.Entity);
34                         }
35                         break;
36                     case EntityState.Deleted:
37                         HandleSoftDelete(entry);
38                         EntityEventHelper.TriggerEntityDeletedEvent(entry.Entity);
39                         break;
40                 }
41             }
42         }
43 
44         private void SetCreationAuditProperties(DbEntityEntry entry)
45         {
46             if (entry.Entity is IHasCreationTime)
47             {
48                 entry.Cast<IHasCreationTime>().Entity.CreationTime = DateTime.Now;
49             }
50 
51             if (entry.Entity is ICreationAudited)
52             {
53                 entry.Cast<ICreationAudited>().Entity.CreatorUserId = AbpSession.UserId;
54             }
55         }
56 
57         private void SetModificationAuditProperties(DbEntityEntry entry)
58         {
59             if (entry.Entity is IModificationAudited)
60             {
61                 var auditedEntry = entry.Cast<IModificationAudited>();
62 
63                 auditedEntry.Entity.LastModificationTime = DateTime.Now;
64                 auditedEntry.Entity.LastModifierUserId = AbpSession.UserId;
65             }
66         }
67 
68         private void HandleSoftDelete(DbEntityEntry entry)
69         {
70             if (entry.Entity is ISoftDelete)
71             {
72                 var softDeleteEntry = entry.Cast<ISoftDelete>();
73 
74                 softDeleteEntry.State = EntityState.Unchanged;
75                 softDeleteEntry.Entity.IsDeleted = true;
76 
77                 if (entry.Entity is IDeletionAudited)
78                 {
79                     var deletionAuditedEntry = entry.Cast<IDeletionAudited>();
80                     deletionAuditedEntry.Entity.DeletionTime = DateTime.Now;
81                     deletionAuditedEntry.Entity.DeleterUserId = AbpSession.UserId;
82                 }
83             }
84         }

复制代码

 

然后再看EntityEventHelper.TriggerEntityCreatingEvent的实现代码:

复制代码

1         public void TriggerEntityCreatingEvent(object entity)
2         {
3             var entityType = entity.GetType();
4             var eventType = typeof(EntityCreatingEventData<>).MakeGenericType(entityType);
5             var eventData = (IEventData)Activator.CreateInstance(eventType, new[] { entity });
6             EventBus.Trigger(eventType, eventData);
7         }

复制代码

就是通过框架的EventBus触发了一个事件,然后在Fami项目里捕获这个事件:

复制代码

 1     public class EntityCreatingEventHandler : IEventHandler<EntityCreatingEventData<Entity>>, ITransientDependency2     {3         private readonly IAbpSession _session;4 5         public EntityCreatingEventHandler(IAbpSession session)6         {7             _session = session;8         }9 
10         public void HandleEvent(EntityCreatingEventData<Entity> eventData)
11         {
12             autoFillRelationId(eventData.Entity);
13         }
14 
15         //新增实体时,自动填入关联的TenantId、xxxxId
16         private void autoFillRelationId(Entity entity)
17         {
18             if (entity is IMustHaveTenant)
19             {
20                 ((IMustHaveTenant)entity).TenantId = _session.GetTenantId(); 
21             }
22             ...... //这里把其他代码删掉了
23         }
24 
25     }

复制代码

这样就自动赋值了,当然前提是这个实体实现了IMustHaveTenant接口,我写了相应基类自动实现了这个接口。

1     public interface IMustHaveTenant
2     {
3         Guid TenantId { get; set; }
4     }
1     public abstract class AuditedEntityAndTenant : AuditedEntity, IMustHaveTenant, IFilterByTenant
2     {
3         [Index]
4         public virtual Guid TenantId { get; set; }
5     }

 

再说第2个,查询时自动实现TenantId的过滤:

已经有更新的方式实现,所以把以前的回答内容删除了。

现在用了EntityFramework.DynamicFilters组件实现自动过滤。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/439165.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CCNA-Cisco-Packet-Tracerchs(思科官网)安装教程以及使用

Cisco-Packet-Tracerchs 思科官网模拟器&#xff0c;各大高校与中职都在使用。 但是由于支持的命令不太多&#xff0c;适用于NA阶段以及入门阶段的人群使用。 以下是免费百度网盘链接&#xff1a; https://pan.baidu.com/s/136fsYRnAfzGoj0DsQFaYTg 提取码: qa4e 复制这段内容…

CCNA-VLAN讲解与交换机三种端口模式(Acess,Trunk,Hybrid)小白入门级

VLAN讲解与三种端口模式(Acess,Trunk,Hybrid) TAG:所有文章均为原创&#xff0c;可以转载但请声明&#xff0c;是在学校里面做的&#xff0c;使用不了EVE和ENSP&#xff0c;请各路大神嘴下留情&#xff0c;如文章内容有误导请及时联系博主----来自一个17岁的中专生。 1.什么是V…

【转】2015-新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序

在博客园学习很长时间了&#xff0c;今天终于自己也开通了博客&#xff0c;准备分享一些感悟和经验。首先感谢博客园园主提供了这么好的程序员学习交流平台&#xff0c;也非常感谢张善友、dax.net、netfocus、司徒正美 等技术大牛的无私分享&#xff0c;从他们身上学到了很多。…

工作篇-佛山三水恒大-2020.10.23

** 工作篇-佛山三水恒大-2020.10.23 **人生第一次工作啊啊啊啊啊 其实按道理来说&#xff0c;第一次应该会很很紧张吧&#xff0c;不过我倒没有。 也不知道是我不怕还是什么&#xff0c;一切进行的挺顺利的。 前言&#xff1a;讲真那句&#xff0c;第一次做的话&#xff0c;…

UWP 使用OneDrive云存储2.x api(一)【全网首发】

最近开发人脸识别UWP【微识别 / WeRecognition】用到了OneDrive开发&#xff0c;下面把来龙去脉讲一下。 下载地址 https://www.microsoft.com/store/productId/9PDSNS7X9ST9&#xff0c;商店火爆热销中。。。。。。 由于UWP是跨平台的&#xff0c;一套代码可以运行在所有Wind…

UWP 使用OneDrive云存储2.x api(二)【全网首发】

上一篇提到为了给用户打造一个完全无缝衔接的最佳体验&#xff0c;UWP开发者最好也要实现App设置和数据的跨平台 分析了数据漫游和OneDrive的优缺点&#xff0c;结合自己App实际需要&#xff0c;我选择了OneDrive。 毕竟数据漫游100KB不够用啊。。。 这一次给大家我千辛万苦找…

在Sharepoint 2010中使用ReportViewer控件展示RDLC报表

微软的Visual studio提供了ReportViewer控件以及RDLC报表设计工具。下文主要介绍如何在Sharepoint 2010项目开发中使用ReportViewer和RDLC生成项目报表。由于Sharepoint 2010默认是禁用Session的&#xff0c;而RDLC报表必须启用Session。因此在Sharepoint的站点中要使用RDLC生成…

工作篇-佛山三水恒大-2020.11.13

** 工作篇-佛山三水恒大-2020.11.14 **TAG:此篇文章估计会很长,因为工作的时候变数太多了,预计五千字左右,想看的可以耐心看完,均为个人实战经验.害,其实是上学期间请假去做的,还挨批了. **到了现场tm的那个机房,小到我哭,站都没地方站,刚装修好全tm都是白灰,我穿的一身黑衣…

Windows Workflow Foundation(WWF)介绍

Windows Workflow Foundation&#xff1a;支持基于工作流的应用程序 工作流是一个简单思路&#xff1a;按照特定顺序执行的一系列步骤。您甚至可以认为每个应用程序都在执行工作流&#xff0c;因为每个应用程序都执行某些过程。但是&#xff0c;在使用 C#、Visual Basic 或其他…

CCNA-第五篇-基础命令集+设备升级+设备破解密码+IP地址{精髓篇}

** CCNA-第五篇-基础命令集 ** 1.基础命令集 思科设备 : IOS 华为设备: VRP 启动步骤&#xff1a;加电自检-加载系统(IOS/VRP&#xff09;-运行配置&#xff08;保留的cfg或者conf文件&#xff09; 命令行统一称为CLI,CLI是啥呢,命令 简介&#xff1a;CLI一般指命令行界面。…

你还记得windows workflow foundation吗

很多年前&#xff0c;windows workflow foundation还叫WWF&#xff0c;而直译过来的名称让很多人以为它就是用来开发工作流或者干脆就是审批流的。 博主当年还是个懵懂的少年&#xff0c;却也知道微软不会大力推一个面向如此具象的业务场景的技术&#xff0c;于是特地找了一本…

SharePoint Desiger编辑模板时提示“服务器错误,拒绝访问”的解决之道

这篇文件已被微软收藏。http://technet.microsoft.com/zh-cn/ff683721.aspx各位同志好&#xff0c;很多同志都用过SharePoint designer来编辑模板。经常出现“服务器错误&#xff0c;拒绝访问”。场景描述&#xff1a;我们办公电脑是XP系统。XP系统我们是用自己的域用户名密码登…

CCIE理论-第六篇-SD-WAN网络(一)

** CCIE理论-第六篇-SD-WAN网络 ** 1.SD-WAN介绍 1.什么是SD-WAN SD-WAN Software Defined 软件定义WANWide Area Network 广域网外网通俗的说企业网关可以慢慢的代替传统组网(大趋势)主要作用,省钱,简单,智能.易管理 传统方式 -Internet专线 (固定IP上下行带宽对等) -PPPOE …

【转】TFS测试管理

微软2010年发布的Visual Studio 2010或Visual Studio Test Professional 2010包含一个称为 Microsoft 测试管理器的新应用程序&#xff0c;用于帮助您使用测试计划来定义和管理测试工作。 Microsoft 测试管理器通过Team Foundation Server 集成&#xff0c;使您可以方便地进行测…

CCNA-网络常用工具介绍篇

链接&#xff1a;https://pan.baidu.com/s/1Mo3B9LR6YF4YfzSkMwn5OA 提取码&#xff1a;7dc7 这是免费提供滴工具,虽然其实都能用到,不过也就发发了. 第一个呢是EVE,是基于VM下的,就是模拟器来的.里面有ova直接拉进去vm里面开机就可以使用了.前面好像也发过 第二个是ENSP,是华…

敏捷项目管理过程改进

一、为什么敏捷&#xff1f; 目前大环境智慧城市、人工智能、大数据&#xff0c;面向To B的业务等&#xff0c;在要求产品管理需要快速的需求响应&#xff0c;项目管理需要更强的整合协调。复杂的大环境&#xff0c;就在推动我们用最敏捷的方式迎接这个多变的市场。 二、传统和…

CCNA-第六篇-静态路由+动态路由开头

** CCNA-第六篇-静态路由动态路由 ** 一,路由概念 什么是路由? 路由呢,应该说是除了IP以外在网络世界中最重要的东西了 万物互联,互联网,都是基于路由的,前期的啥静态动态,后期的MPLS,包括看到的VPN,虚拟专线等.都是基于路由的,就是像建房子那样,你最得把地基打好才能做其…

【转】敏捷开发,你真的做对了吗?

缘起 2017年3月&#xff0c;应移动事业群智能营销平台项目管理部负责人邀请&#xff0c;我开始支持智能营销平台CRM团队。智能营销平台是阿里文娱广告团队&#xff0c;是阿里巴巴淘外变现的主力军。CRM团队负责开发和维护CRM系统。CRM系统服务于销售和代理商&#xff0c;串起商…

CCNA-第七篇-思科私有路由协议-EIGRP-初级

CCNA-第七篇-思科私有路由协议-EIGRP 首先呢这个EIGRP之前呢, 路由协议是分几种的 一个叫距离向量协议RIP,IGRP(都过时了) 一个觉链路状态协议OSPF,IS-IS这些 还有个叫混合型的EIGRP 但是呢,这些只是书本上的定义,实际上没人会跟你说这个东东 这个怎么区分呢? 第一个呢,只传递…

CCNA-第八篇-OSPF-上

CCNA-第八篇-OSPF OSPF,最常用的路由协议,他来了他来了 OSPF呢怎么说呢 是一个比较重要而且比较基础的点,出到去外面要是说不会OSPF,那还算啥网络工程师 但是呢,他也不是那么的完全重要.因为很多小地方压根就用不到.但是列你不能不会呀 到了OSPF呢,配置就会逐渐的多那么一点点…