今天在@張善友和@田園裡的蟋蟀的部落格看到微軟“.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、繼承自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.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 })
介面截圖:
在進行這個列表查詢時,客戶端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 #1 2 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>>, ITransientDependency 2 { 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元件實現自動過濾。