开发现代ASP.NET应用程序
新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序(续1)
今天在@张善友和@田园里的蟋蟀的博客看到微软“.Net社区虚拟大会”dotnetConf2015的信息,感谢他们的真诚付出!真希望自已也能为中国的.NET社区贡献绵薄之力。
上周星期天开通了博客并发布了第一篇文章《新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序》,汇集了一些比较流行的技术和开源项目,也把自己的程序架构、部分代码风格、前端表现简单做了一些展示,引起了近100位朋友的评论。特别感谢@田园里的蟋蟀、@深蓝医生、@郭明锋、@疯狂的提子、@jimcsharp、@以吾之名等给我建议和指导的朋友,也感谢那些给我支持和鼓励的朋友。还有对我提出批评的朋友,说我的面试题的内容不当,也很感谢他们让我更注意言辞,但并不会影响我对面试者基础知识的重视程度。
上周发布那篇文章主要是因为这段时间在招聘过程中发现几乎所有面试者对基础知识和新技术都知之甚少,有过几年工作经验的程序员也几乎只会单一模式的CURD,没有明显的技术特长,所以我想分享一些自己认为比较好的思想、技术、架构模式,引起更多ASP.NET程序员的思考和讨论。
其实,上周星期天是花了大半天写一篇博客,在发出来之前删掉了一大半内容(一些讲述我自己心路历程的内容),因为我在博客园是一个新人,在没有对别人提供价值帮助之前也许没人关心我是谁。那天由于时间太晚了,很多想写的内容都没有写出来,发布的时候仅贴了一些图片,后来在评论中写了很多内容,并修改了原文正文,补充分享了一些非常好的开源项目。希望之前看过的朋友可以再回去看看,给个链接:http://www.cnblogs.com/mienreal/p/4340864.html
之前的一个项目是做的微信公众平台的第三方平台,提供微网站自主建站、会员卡、微商城、外卖预订等几十项功能。在项目初期,我仅担任产品总监负责产品设计,后来因为没有强大的前端团队,不得不亲自实现微官网的可视化设计器的前端。再后来公司让我接管了开发部(全是JAVA开发人员),跟开发团队有了更直接的配合。我发现他们普遍代码质量不高,几乎不懂得运用设计模式和最佳实践。每新增或修改一点功能,都要将全部代码进行编译和发布,会影响正在登录使用的用户,而且有时候一个经验不足的程序员修改的一点东西会让整个平台不能正常启动。跟几个高级工程师多次沟通,希望他们学习新技术新思想,运用成熟的最佳实践来提高代码质量;希望他们了解领域驱动设计用于会员卡等业务较复杂的模块;希望他们能了解OSGI实现模块化开发和部署,但因为经验能力和积极性等原因,这些愿望都没有实现。后来在新项目(开发代号Fami)中,我选择了.NET技术平台,并组建新的开发团队来进行这个项目。现在项目才刚完成基础框架和项目规范。
下面把这个项目的架构思想和功能特性再分享一下。希望对正在设计架构的朋友有一个参考作用。本项目是Saas模式的在线产品,需实现多租户模式;有多个功能模块,且上线时间有先有后,需实现模块化开发。
本项目总体分为两个部分:一个基础框架组件,一个Fami解决方案。
基础框架组件的功能:
1、基础框架组件独立、通用,可用于多个不同项目。类似于daxnet的Apworks框架。
2、对项目实现模块化开发提供了支持,每个模块有独立的EF DbContext,可单独指定数据库。
3、对DDD的技术实现进行了封装,让项目以极精简的代码,专注于业务领域。
4、多租户支持,每个租户的数据自动隔离,业务模块开发者不需要手动操作TenantId。
5、集成ASP.NET Identity,实现登录认证、功能权限授权&验证、角色和用户管理。
6、集成Log4Net,实现日志记录。
7、集成AutoMapper,实现Dto类与实体类的双向自动转换。
8、实现UnitOfWork模式,为应用层和仓储层的(会写数据库的)方法自动实现数据库事务。
9、可通过ApplicationService的方法自动建立相应的WebApi方法,ajax可直接调用,不需要写ApiController和Action。
10、调用ApplicationService的方法时,自动验证权限和参数有效性(用相应的Attribute标注)。
11、继承自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的过滤:
自动过滤的实现方式很简单,在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语句。
开发现代ASP.NET应用程序的更多相关文章
- 新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序
在博客园学习很长时间了,今天终于自己也开通了博客,准备分享一些感悟和经验.首先感谢博客园园主提供了这么好的程序员学习交流平台,也非常感谢张善友.dax.net.netfocus.司徒正美 等技术大牛的 ...
- C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式
C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...
- 逐步改用 IronPython 开发你的 ASP.NET 应用程序
IronPython for ASP.NET 的 CTP 已经发布有一段时间了,我们在看了官方提供的范例之后,相信对一个 ASP.NET 应用程序中完全使用 IronPython 开发还是有一些担心的 ...
- ASP.NET开发实战——(一)开篇-用VS创建一个ASP.NET Web程序
本文是本系列文章第一篇,主要通过建立一个默认ASP.NET MVC项目来引出与ASP.NET MVC相关的功能,由于ASP.NET MVC一个简单的模板就具备了数据库操作.身份验证.输入数据校 ...
- 【译】ASP.NET应用程序和页面生命周期
为何翻译此文 一.此文是Code Project社区2010年4月ASP.NET板块的最佳文章,说明了此文的份量: 二.锻炼自己的英文技术文章翻译能力,提高英文技术文档阅读能力: 三.了解掌握ASP. ...
- 提高ASP.NET应用程序性能的十大方法
一.返回多个数据集 检查你的访问数据库的代码,看是否存在着要返回多次的请求.每次往返降低了你的应用程序的每秒能够响应请求的次数.通过在单个数据库请求中返回多个结果集,可以减少与数据库通信的时间,使你的 ...
- 使用VS Code开发调试ASP.NET Core 1.0
使用VS Code开发调试ASP.NET Core 1.0,微软在今天凌晨发布了.NET Core 1.0,ASP.NET Core 1.0 与 Entity Framewok 1.0. 之前跟大家讲 ...
- 构建高性能的ASP.NET应用程序
看见大标题的时候,也许各位看官会自然而然的联想到如何在设计阶段考虑系统性能问题,如何编写高性能的程序代码.关于这一点,大家可以在MSDN和相关网站上找到非常多的介绍,不过大多是防患于未难,提供的是在设 ...
- SNF开发平台WinForm之十一-程序打包-SNF快速开发平台3.3-Spring.Net.Framework
原来我们用的是微软自带的打包工具去打包,但感觉好像也是第三方做的打包并且很是麻烦,还有时不成功报错.那综合考虑就找一个简单实用的打包工具吧,就找到了NSIS这个.具体打包步骤如下: 1.安装NSIS ...
随机推荐
- Singleton模式(Singleton创建类型)c#简单的例子
单(Singleton创建模式)c#简单的例子 当需要生成一个实例,可单发模式 样品可以在短短的球员中产生,玩家和测试.单线程例子,如以下: namespace singletonpattern { ...
- uav 11258 String Partition (DP)
Problem F - String Partition ...
- Lua 环境结构 --Linux
curl -R -O http://www.lua.org/ftp/lua-5.2.3.tar.gz tar zxf lua-5.2.3.tar.gz cd lua-5.2.3 make linux ...
- [创意标题] spoj 11354 Amusing numbers
意甲冠军: 给k(1<=k<=10^15),先询问k 大只包含数字5和6的数目是多少 实例 1那是,5 ,3那是,55 .4那是,56 思考: 首先,我们可以找到.有许多2这是头号,有两个 ...
- POJ1458 Common Subsequence 【最长公共子序列】
Common Subsequence Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 37614 Accepted: 15 ...
- WPF学习(7)命令
在上一篇中,我们学习了WPF的路由事件,而在本节将学习一个更为抽象且松耦合的事件版本,即命令.最明显的区别是,事件是与用户动作相关联的,而命令是那些与用户界面想分离的动作,例如我们最熟悉的剪切(Cut ...
- 谈论高并发(十二)分析java.util.concurrent.atomic.AtomicStampedReference看看如何解决源代码CAS的ABA问题
于谈论高并发(十一)几个自旋锁的实现(五岁以下儿童)中使用了java.util.concurrent.atomic.AtomicStampedReference原子变量指向工作队列的队尾,为何使用At ...
- Bye,IE!服务互联网20年IE终于要退役了
美国当地时间16日中午,Microsoft Edge官微发表了祝词:Internet Explorer 20岁生日快乐!你在过去做出了巨大贡献,今后由我继续发扬光大.服务互联网20年之后,IE终于要退 ...
- IOS 在功能 autorelease release
在一个reurn 返回值该函数里 假设,再申请为期内存(alloc 要么copy) 这次不行release 您只能使用autorelease 返回到接收在该指针后,它是由被释放!! 假设self.o ...
- 网站的SEO以及它和站长工具的之间秘密(转)
博客迁移没有注意 URL 地址的变化,导致百度和 google 这两只爬虫引擎短时间内找不到路.近段时间研究了下国内最大搜索引擎百度和国际最大搜索引擎google的站长工具,说下感受. 百度的站长工具 ...