在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
今天在@张善友和@田园里的蟋蟀的博客看到微软“.Net社区虚拟大会”dotnetConf2015的信息,感谢他们的真诚付出!真希望自已也能为中国的.NET社区贡献绵薄之力。
本项目总体分为两个部分:一个基础框架组件,一个Fami解决方案。
基础框架组件的功能:
Fami项目解决方案结构图:
每个模块是一个独立的类库项目,有独立的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 > 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 ></i>新增文章</button> 30 <button ></i>编辑</button> 31 <button ></i>删除 </button> 32 <button ></i>刷新 </button> 33 </div> 34 <table ></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) 这样就自动赋值了,当然前提是这个实体实现了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的过滤: 自动过滤的实现方式很简单,在Fami项目的仓储基类FamiRepository.cs中重写了基础框架组件仓储基类EfRepositoryBase的GetAll()方法 1 namespace Fami.Core 2 { 3 public class FamiRepository<TDbContext, TEntity, TPrimaryKey> : EfRepositoryBase<TDbContext, TEntity, TPrimaryKey> 4 where TEntity : class, IEntity<TPrimaryKey> 5 where TDbContext : DbContext 6 { 7 public FamiRepository(IDbContextProvider<TDbContext> dbContextProvider) 8 : base(dbContextProvider) 9 { 10 } 11 12 //整个解决方案共用的仓储方法写在这里 13 14 15 16 public override IQueryable<TEntity> GetAll() 17 { 18 var query = Table as IQueryable<TEntity>; 19 //LYM 每个查询前都根据接口添加过滤条件 20 if (typeof(IFilterByTenant).IsAssignableFrom(typeof(TEntity))) 21 { 22 query = query.Where(createEqualityExpression<TEntity, Guid>("TenantId", _session.GetTenantId())); 23 } 24 25 return query; 26 } 27 28 private static Expression<Func<TEntity, bool>> createEqualityExpression<TEntity, TType>(string keyName, TType value) where TEntity : class//, IEntity 29 { 30 var lambdaParam = Expression.Parameter(typeof(TEntity), "entity"); 31 32 var lambdaBody = Expression.Equal( 33 Expression.PropertyOrField(lambdaParam, keyName), 34 Expression.Constant(value, typeof(TType)) 35 ); 36 37 return Expression.Lambda<Func<TEntity, bool>>(lambdaBody, lambdaParam); 38 } 39 40 } 41 42 public class FamiRepository<TEntity, TPrimaryKey> : EfRepositoryBase<CoreDbContext, TEntity, TPrimaryKey> 43 where TEntity : class, IEntity<TPrimaryKey> 44 { 45 public FamiRepository(IDbContextProvider<CoreDbContext> dbContextProvider) 46 : base(dbContextProvider) 47 { 48 } 49 50 } 51 52 public class FamiRepository<TEntity> : FamiRepository<CoreDbContext, TEntity, Guid>, IFamiRepository<TEntity> 53 where TEntity : class, IEntity 54 { 55 public FamiRepository(IDbContextProvider<CoreDbContext> dbContextProvider) 56 : base(dbContextProvider) 57 { 58 } 59 60 } 61 }
以后在使用FamiRepository<TEntity>类型调用任何查询方法,都会根据TenantId进行过滤。这种方法暂时只能对聚合根实体过滤TenantId。 您可以看看上面文章查询列表的AppService方法和生成的SQL语句。
Bootstrap
今天在@张善友和@田园里的蟋蟀的博客看到微软“.Net社区虚拟大会”dotnetConf2015的信息,感谢他们的真诚付出!真希望自已也能为中国的.NET社区贡献绵薄之力。
本项目总体分为两个部分:一个基础框架组件,一个Fami解决方案。
基础框架组件的功能:
Fami项目解决方案结构图:
每个模块是一个独立的类库项目,有独立的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 > 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 |
请发表评论