今天在@張善友和@田園裏的蟋蟀的博客看到微軟「.Net社區虛擬大會」dotnetConf2015的信息,感謝他們的真誠付出!真但願自已也能爲中國的.NET社區貢獻綿薄之力。html
上週星期天開通了博客併發布了第一篇文章《新思想、新技術、新架構——更好更快的開發現代ASP.NET應用程序》,聚集了一些比較流行的技術和開源項目,也把本身的程序架構、部分代碼風格、前端表現簡單作了一些展現,引發了近100位朋友的評論。特別感謝@田園裏的蟋蟀、@深藍醫生、@郭明鋒、@瘋狂的提子、@jimcsharp、@以吾之名等給我建議和指導的朋友,也感謝那些給我支持和鼓勵的朋友。還有對我提出批評的朋友,說個人面試題的內容不當,也很感謝他們讓我更注意言辭,但並不會影響我對面試者基礎知識的重視程度。前端
上週發佈那篇文章主要是由於這段時間在招聘過程當中發現幾乎全部面試者對基礎知識和新技術都知之甚少,有過幾年工做經驗的程序員也幾乎只會單一模式的CURD,沒有明顯的技術特長,因此我想分享一些本身認爲比較好的思想、技術、架構模式,引發更多ASP.NET程序員的思考和討論。程序員
其實,上週星期天是花了大半天寫一篇博客,在發出來以前刪掉了一大半內容(一些講述我本身心路歷程的內容),由於我在博客園是一個新人,在沒有對別人提供價值幫助以前也許沒人關心我是誰。那天因爲時間太晚了,不少想寫的內容都沒有寫出來,發佈的時候僅貼了一些圖片,後來在評論中寫了不少內容,並修改了原文正文,補充分享了一些很是好的開源項目。但願以前看過的朋友能夠再回去看看,給個連接:http://www.cnblogs.com/mienreal/p/4340864.html 面試
以前的一個項目是作的微信公衆平臺的第三方平臺,提供微網站自主建站、會員卡、微商城、外賣預訂等幾十項功能。在項目初期,我僅擔任產品總監負責產品設計,後來由於沒有強大的前端團隊,不得不親自實現微官網的可視化設計器的前端。再後來公司讓我接管了開發部(全是JAVA開發人員),跟開發團隊有了更直接的配合。我發現他們廣泛代碼質量不高,幾乎不懂得運用設計模式和最佳實踐。每新增或修改一點功能,都要將所有代碼進行編譯和發佈,會影響正在登陸使用的用戶,並且有時候一個經驗不足的程序員修改的一點東西會讓整個平臺不能正常啓動。跟幾個高級工程師屢次溝通,但願他們學習新技術新思想,運用成熟的最佳實踐來提升代碼質量;但願他們瞭解領域驅動設計用於會員卡等業務較複雜的模塊;但願他們能瞭解OSGI實現模塊化開發和部署,但由於經驗能力和積極性等緣由,這些願望都沒有實現。後來在新項目(開發代號Fami)中,我選擇了.NET技術平臺,並組建新的開發團隊來進行這個項目。如今項目纔剛完成基礎框架和項目規範。ajax
下面把這個項目的架構思想和功能特性再分享一下。但願對正在設計架構的朋友有一個參考做用。本項目是Saas模式的在線產品,需實現多租戶模式;有多個功能模塊,且上線時間有先有後,需實現模塊化開發。sql
本項目整體分爲兩個部分:一個基礎框架組件,一個Fami解決方案。數據庫
基礎框架組件的功能:
一、基礎框架組件獨立、通用,可用於多個不一樣項目。相似於daxnet的Apworks框架。
二、對項目實現模塊化開發提供了支持,每一個模塊有獨立的EF DbContext,可單獨指定數據庫。
三、對DDD的技術實現進行了封裝,讓項目以極精簡的代碼,專一於業務領域。
四、多租戶支持,每一個租戶的數據自動隔離,業務模塊開發者不須要手動操做TenantId。
五、集成ASP.NET Identity,實現登陸認證、功能權限受權&驗證、角色和用戶管理。
六、集成Log4Net,實現日誌記錄。
七、集成AutoMapper,實現Dto類與實體類的雙向自動轉換。
八、實現UnitOfWork模式,爲應用層和倉儲層的(會寫數據庫的)方法自動實現數據庫事務。
九、可經過ApplicationService的方法自動創建相應的WebApi方法,ajax可直接調用,不須要寫ApiController和Action。
十、調用ApplicationService的方法時,自動驗證權限和參數有效性(用相應的Attribute標註)。
十一、繼承自FullAuditedEntity基類的領域實體,會自動實現軟刪除(在數據庫中用IsDeleted字段進行標註)。
十二、實現一系列擴展方法,簡化編碼。設計模式
Fami項目解決方案結構圖:api
模塊化結構圖 | 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補充 回覆@何鎮汐 多租戶機制的自動實現:
自動實現兩方面的操做:
一、新建實體時自動從當前用戶的session中取出所屬的租戶標識(TenantId) 給實體的TenantId賦值
二、查詢數據時自動根據當前用戶的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組件實現自動過濾。