刚刚检查完支持工单中的一些代码,笔者想针对 ASP.NET MVC 应用的改进写一些建议。这些内容仍在笔者脑海中,愿与各位一同分享。若你已使用 MVC 一段时间,那么以下内容可能并不新鲜。本文更适用于不常使用 MVC 或尚未充分了解 MVC 的读者。
假设以下场景:你想弄清楚一个网络应用在生产环境下为何消耗了 Web 服务器2GB 内存,于是,你将生产环境中运行的应用版本部署到本地运行,用于分析和调试。
仔细查看代码后,你认真地分析,可能还时不时摇摇头,最终弄清了问题的本质,那么此时,你应该给出反馈了。
这就是笔者今天的经历,从中总结出5点建议,希望能使读者在使用 ASP.NET MVC 代码时更加得心应手。
笔者收到的支持工单,其根本原因在于,从数据库中提取了大量数据,导致占用了过量内存。
这一问题十分常见。假如你建立了一个普通的博客,其中包含了文章以及多种媒体(图片、视频、附件)。你将一个 Media 数组放到 Post 域对象中,后者将所有图片数据储存在一个字节数组中。由于你使用了 ORM,因此需要采用某种方法将域模型设计完善;我们都经历过这一步。
public class BlogPost { public ICollection<BlogMedia> Media { get; set; } } public class BlogMedia { public byte[] Data { get; set; } public string Name { get; set; } }
这种设计并没有大的不妥,你很准确地建立了域模型。但问题在于,当你通过最常用的 ORM 发起查询时,所有与博客文章相关的数据都会被加载出来。
public IList<BlogPost> GetNewestPosts(int take) { return _db.BlogPosts.OrderByDescending(p => p.PostDate).Take(take).ToList(); }
这一行看起来毫无问题(除非你曾受其困扰,所以了解它并非无害),但如果不取消延迟加载或没让 ORM 忽略日志媒体上的大「Data」属性,那么就可能导致非常严重的后果。
你应当了解 ORM 是如何进行查询和映射对象的,并确保所查询内容就是需要的内容(比如使用 projection),这一点十分重要。
public IList<PostSummary> GetNewestPosts(int take) { return _db.BlogPosts.OrderByDescending(p => p.PostDate).Take(take).Select(p => new PostSummary() { Title = p.Title, Id = p.Id }).ToList(); }
这能确保只抓取任务真正需要的数据量。如果你要做的仅仅是使用标题和 ID 在主页上建立一个链接, 那么得到这俩属性就够了 。
你可以在知识库中准备5种以上的方法,为使用户界面更加完善,再仔细也不为过。
这一条比较难注意到。设想 MVC 视图中的以下代码:
@foreach(var post in Model.RelatedPosts) { ... }
看起来 没什么问题,但如果仔细看看这一模型属性中隐含的内容呢?
public class MyViewModel { public IList<BlogPost> RelatedPosts { get { return new BlogRepository().GetRelatedPosts(this.Tags); } } }
呀!「视图模型」中含有业务逻辑,此外还直接调用了一个数据存取方法。如此一来,数据存取代码被引入了陌生的区域,并隐藏在属性中。将此代码移动到控制器中,便于对其进行讨论并有意识地为视图模型添加内容。
此处正好说明一下,适当的单元测试可帮助发现此类问题;由于肯定不能拦截对这此类方法的调用,你可能会恍然大悟,不该将知识库注入视图模型中。
如需在视图中执行业务逻辑,那就应重新考虑视图模型和逻辑。不建议在 MVC Razor 视图中执行此类操作。
@{ var blogController = new BlogController(); } <ul> @foreach(var tag in blogController.GetTagsForPost(p.Id)) { <li>@tag.Name</li> } </ul>
切勿在视图中使用业务逻辑,但除此之外,你可以创建一个 控制器 !将业务逻辑移动到动作方法中,并将视图模型用于原本的用途。还可以将业务逻辑移动到单独的动作方法中,这一动作方法仅在视图内被调用,这样就可在必要时单独对其进行缓存。
//In the controller: [ChildActionOnly] [OutputCache(Duration=2000)] public ActionResult TagsForPost(int postId) { return View(); } //In the view: @{Html.RenderAction("TagsForPost", new { postId = p.Id });}
注意 「ChildActionOnly」 属性。 MSDN 中提到:
任何一个标有 「ChildActionOnlyAttribute」的方法都只能与 「Action」或「RenderAction」HTML 扩展方法一同被调用。
这就意味着,没有人能通过操作 URL 来访问你的子动作(如果你采用了默认路径)。
在 MVC 库中,局部模块和子动作都是很有用的工具,所以充分利用起来吧!
有了以上的代码做铺垫,如果只缓存视图模型,又会有怎样的效果呢?
public ActionResult Index() { var homepageViewModel = HttpContext.Current.Cache["homepageModel"] as HomepageViewModel; if (homepageViewModel == null) { homepageViewModel = new HomepageViewModel(); homepageViewModel.RecentPosts = _blogRepository.GetNewestPosts(5); HttpContext.Current.Cache.Add("homepageModel", homepageViewModel, ...); } return View(homepageViewModel); }
什么效果也没有!由于是通过视图中的控制器变量和视图模型中的属性进入数据层,因此并不能提升性能……缓存视图模型并没有什么用处。
试试缓存 MVC 动作的 输出 吧:
[OutputCache(Duration=2000)] public ActionResult Index() { var homepageViewModel = new HomepageViewModel(); homepageViewModel.RecentPosts = _blogRepository.GetNewestPosts(5); return View(homepageViewModel); }
请注意非常好用的「OutputCache」属性。MVC 支持 ASP.NET 输出缓存,因此请在适当情况下,充分利用这一特点。如需缓存模型,那么模型基本上应为带自动(且只读)属性的 POCO,不能调用其他知识库方法。
另外还想介绍笔者尚未尝试的一个好方法,即 采用不同的输出缓存供应商 ,从而在AppFabric、NoSQL 或其他任何需要的地方进行缓存。MVC 的可扩展性非常强。
如果不好好利用 ORM 的特征集,那真是极大的损失。笔者所检查的代码库中用到了 NHibernate,但是并未真正利用好。本可以用来解决一部分内存问题的 NHibernate 高级射影功能 完全被忽略了。这一问题有时是因为使用“库模式”所造成的僵化思维,有时则是由于缺乏必要的知识。
与仅仅使用基本的类方法相比,通过利用 EF 或 NHibernate 特征,知识库的功能可以大大增加。它们可以在控制器中形成和返回 你真正想要的 数据,大大增强控制器的逻辑性。赶紧阅读 ORM 文件,了解一下它可以提供的功能吧,这将使你受益良多。
笔者认为,采用知识库模式,就好比驱除掉雾霾,使明媚的阳光从 ORM 窗口照进来。刚接触 RavenDB 时,笔者 丢弃 了知识库层(实际上是 整个数据项目 ),在应用服务层中完全使用 Raven 查询,用了一点点扩展方法来重复使用查询逻辑。笔者发现,许多逻辑都明显依赖于特定的上下文,且利用 Raven 的扩展特性进行投射、形成并分批处理查询,大有益处。
那只是你一家之言……
如果你认为可以将 ORM 抽象化,笔者强烈建议你换个角度思考。ORM 确实是 抽象概念,如果你认为,由于 ORM 是「抽象」的,所以轻而易举就能用别的 ORM 置换现有的 ORM,那么事实会让你大吃一惊。因为我之前也是这么想的,直到我了解到,转换至 Raven 简直改变了我整个代码库,这是我完全没有预料到的。ORM 不仅仅影响到数据存取,还会影响域以及业务逻辑,甚至会影响用户界面。通过移除知识库抽象,可以 切实降低数据存取代码的整体复杂度 。
家父常常拿这句话提醒我。有时候,通过仔细查阅代码,也会发现你认为人人皆知的道理,事实并非如此;你可能从实践经验中了解到这一点,或在 google 上读到这一点,就错误地假设这是人人都知道的事实了。
希望这篇文章能帮到需要的人!
原文地址: http://kamranicus.com/blog/2014/01/29/5-tips-to-improve-your-mvc-site/