原文:http://www.cnblogs.com/sonykings/archive/2013/05/30/3107531.html
ASP.NET MVC最佳实践
本文档提供了一套旨在帮助创建最佳优化ASP.NET MVC应用程序的 开发人员编码准则。 当然,由您来选择这些优化准则。
模型的建议
Model是定义业务领域相关的对象,应该包含业务逻辑(对象如何动作和关联),验证逻辑(验证对象的有效值),数据逻辑(数据对象如何持久化),和会话逻辑(跟踪用户状态)。
创建独立的Model项目,在ASP.NET MVC 项目中引用Model程序集。
尽量将频繁复用的业务逻辑放置在Model中。
如将所有业务逻辑放置在Model项目中,可根据实际业务数据来生成View和Controller。有如下好处:
1.减少重复的业务逻辑。
2.在View中减少业务逻辑,View易于理解。
3.业务逻辑的测试仅仅和Model有关。
例如,下面需要显示用户的用户名 – 先显示Last Name,在View中代码如下:
<% if (String.Compare((string)TempData["displayLastNameFirst"], "on") == 0) { %> Welcome, <%= Model.lastName%>, <%= Model.firstName%> <% } else { %> Welcome, <%= Model.firstName%> <%= Model.lastName%> <% } %>
然而你需要在每一个地方重复这一逻辑。如将这一业务逻辑放置在Model中,可在Model中添加一个属性封装这一逻辑。
public string combinedName { get { return (displayLastNameFirst ? lastName + " " + firstName : firstName + " " + lastName); } private set { ; } }
这样,可大大简化视图代码:
<% Welcome, <%= Model.combinedName %> %>
将所有验证逻辑放置在Model中
所有输入验证应该在Model层,包括Client-side 验证。
可使用ModelState 添加验证检查,代码如下所示:
if (String.IsNullOrEmpty(userName))
{
ModelState.AddModelError("username", Resources.SignUp.UserNameError);
}
不过,更好的办法是使用 System.ComponentModel.DataAnnotations,在Model类的属性上添加attribute,如下所示:
public class User
{
[Required(ErrorMessageResourceName = "nameRequired", ErrorMessageResourceType = typeof(Resources.User))]
public String userName { get; set; }
...
}
为数据访问定义接口
接口用来暴露数据访问类的方法,强化ASP.NET MVC 的松散耦合设计。
可考虑使用Entity Framework 或 LINQ to SQL 创建对数据库的访问类,Entity Framework 和 LINQ to SQL 都支持存储过程。
将所有会话逻辑放置在Model中。
它已超出本文档在模型中存储会话状态的各种机制深入探讨的范围。 作为一个起步的点下面是几个可能性的会话状态存储:
技术 |
优势 |
弱点 |
在过程中 |
没有所需额外的安装程序。 |
如果 web 站点需要扩展,起作用。 |
会话状态服务 |
轻量 sertvice 在 web 场中的每台计算机上运行。 更快地数据库会话存储。 |
如果该服务出现故障,会话数据将丢失。 |
数据库 |
会话数据保持不变。 |
会话状态较慢。 管理成本也较高。 |
视图建议
View用来展示Model数据,Controller负责选择View。业务逻辑不属于View,Model负责业务逻辑。View非常灵活,如Model的View可通过HTML显示,同样的Model也可通过XML 视图来呈现。
将HTML放置在View和Partial View中(不要在Controller中)
默认的ASP.NET视图引擎提供了如下视图文件:HTML View(.aspx),Partial HTML View(.ascx)和Master page(.master)
如下视图演示了对partial view的调用:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
…
Below is a list of items submitted by <b>
<%= Html.Encode(ViewData["name"]) %></b>.
<p>
...
<div id="items">
<% Html.RenderPartial("ItemsByName");%>
</div>
</asp:content>
Partial view(ItemsByName.ascx)如下所示:
<%@ Control Language="C#" %>
…
<% foreach (Seller.Controllers.Items item in (IEnumerable)ViewData.Model)
{ %>
<tr>
<td>
<%= Html.Encode(item.title)%>
</td>
<td>
<%= Html.Encode(item.price)%>
</td>
</tr>
<% } %>
</table>
<% } %>
Partial View 是一个强大的扩展和重用机制。你可在不同的地方包含相同的View,不必编写重复的代码。
在View中使用ViewData访问数据
ASP.NET 提供了如下机制在View模板中访问数据:
ViewData.Model 对象 – 在Controller的action方法中,在return语句中传入一个Model对象(return View(myModelObject))。
ViewData Dictionary – 在action方法中存入数据(ViewData[“key”] = value),接着在View中方法相同的dictionary。
在可能的情况下,应该是一ViewData Model,而不是ViewData 来访问数据,因为Model 提供了类型安全。此外,你应在View模板中,使用数据访问机制,而不是Request / Session 来访问。
如需要显示一个对象的多个属性,可使用ViewData.Model,并创建一个强类型View。针对seller详细页面,seller类有name、phone、address、email等等属性,在呈现View之前,你可在Controller中对ViewData.Model 赋值seller对象实例。但是如果是一些零散的数据,如page#、用户名和current time,则一般使用ViewData字典。
在使用模型绑定(Model bingding)时,避免在view中访问数据。
在Controller 中访问数据库,在执行View之前,将从数据库中检索的数据复制给轻量的View Model对象,这样,轻量的View Model对象不必在视图执行时检索数据。
使用(自动生成)客户端验证
从ASP.NET MVC 2 开始,可以很容易添加客户端验证。
(1) 如前所述,在Model层中添加数据验证逻辑;
(2) 确保项目中Scripts目录有如下javascript 文件:MicrosoftAjax.js 和 MicrosoftMvcValidation.js;
(3) 在表单提交页面,添加如下代码:
<script src="<%= Url.Content("~/Scripts/MicrosoftAjax.js") %>" type="text/javascript"></script>
<script src="<%= Url.Content("~/Scripts/MicrosoftMvcValidation.js") %>" type="text/javascript"></script>
(4) 在表单中添加如下代码:
<% Html.EnableClientValidation(); %>
现在如果编辑表单内容,当输入值不合格时,客户端马上进行验证提醒。
在模板中插入server-side 注释
在View模板中使用服务端注释,在HTML呈现时,会剔除。
如下是server-side注释:
<%-- This is a server side template comment --%>
不要在View模板中使用HTML 注释,因为这些注释会呈现在web浏览器中,可被用户看到。
使用HTMLHelper 扩展方法。
System.Web.Mvc.Html 类中包含了很多有用的HTML 扩展方法。
Form 表单生成(BeginForm)
输入字段生成(checkbox、hidden、radio button、textbox)
链接URL生成(ActionLink)
XSS保护(Encode)
尽可能使用这些HTML扩展方法,如下是使用route table创建一个链接:
<%= Html.ActionLink(“Home page”, “Default”) %>
控制器的建议
Controller 和指定的Action方法由路由系统根据匹配的URL规则调用。Controller 接收路由系统的输入参数,包括HTTP 请求上下文(Session、Cookies、Browser等等)。
使用Model Binding,而不是手动解析请求。
ASP.NET MVC 通过Model binding 抽象了许多对象反序列代码,Model binding机制是将Request Context数据通过反射对应到Action方法中定义的对象类型上。
如下代码是Seller类,定义了表单提交的数据:
[csharp] public class Seller { public Int64 ID { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Address { get; set; } }
[/csharp]
如下的是Register View 表单:
[html] < % using (Html.BeginForm())
{ %> < legend>Account Information</legend>
< p> < %= Html.TextBox(“Name”) %> < /p>
< p> < %= Html.TextBox(“Phone”) %> < /p>
< p> < %= Html.TextBox(“Address”) %> < /p>
< p> < input value=”Register” /> < /p> < % } %>
[/html]
Controller 需要Register Action 方法,提供Model binding:
[csharp] public ActionResult Register([Bind(Exclude="ID")] Seller newSeller) { … } [/csharp]
默认的model binder 会寻找类中的每一个属性,以Name 为例:
(1) 先查找Request.Form[“Name”]; (2) 接着RouteData.Value[“Name”]; (3) 接着 Request.QueryString[“Name”]; (4) 最后如没有匹配,则为null;
从Register action方法可以看到,通过默认的model binder,对象属性将被赋值。Model binding系统也会进行验证逻辑,如data annotations attributes。
Model binding系统有丰富的扩展机制,可对object的创建、赋值和验证进行全面定制。
在Action方法中显示命名View。
在action方法中完成必要逻辑后,最后返回ViewResult 或 PartialViewResult对象。如不传入View名称给result 类,View 文件将默认为action 名称。如一个名为Products的Controller,有一个List 的Action方法,在List action方法调用 return View() 方法,且不传入任何参数。MVC 框架将寻找 /Views/Products/List.aspx 视图,如不存在,则继续寻找 /Views/Products/List.ascx。如仍不存在,则尝试 /Views/Shared/List.aspx和/Views/Shared/List.ascx。你可使用/Views/Shared 存放多个Controllers共享的视图。
为了避免混淆,在action方法中,显式命名View,如 return View(“explictViewName”),这样可从不同的Action调用List视图。
在提交表单时使用Post/Redirect/Get(PRG)
根据HTTP POST 和 GET 的定义:
- HTTP GET 用于不更改Model数据;
- HTTP POST 用于更改Model 数据;
下面是一个清晰的描述,在post back的action方法接收表单数据,返回 RedirectToAction(<actionName>),该方法导致一个HTTP 302 (临时跳转),并对<actionName>方法生成GET请求,这就是Post – Redirect – Get 模式。
传统的ASP.NET 表单回传数据,有可能导致数据的重复提交,可通过MVC Post-Redirect-Get模式解决这一问题。
但是,这一模式会造成client-side性能影响,因为redirect跳转会再次请求服务器,需要在性能成本和可用性之间进行判断。
实现HandleUnknownAction 和 HandleError
默认的unknown action响应是404(Not found)错误。可在Controller中重载HandleUnknownAction,针对这一错误实现一个默认视图。此外,可在controller的action方法添加HandleError属性,针对未捕获异常提供标准的错误视图。
路由的建议
ASP.NET MVC 中路由映射 URL 直接到controller,而不是一个特定的文件。默认的路由添加到RouteTable,在Global.ascx文件的Application_Start中定义。该路由表负责映射特定URL到Controller和Action。
当时使用标准路由时,从特定的路由到一般路由进行排序。
路由表是有序的,因此按特定的到一般规则来创建路由。举一个示例,假定你有一个product category需要创建URL:
* http://sellmyproducts/ * http://sellmyproducts/Page# * http://sellmyproducts/category * http://sellmyproducts/category/Page#
假定List方法定义如下(在ProductsController 类中): Public ViewResult List(string category, int page)
基于之前指定的URL,下面的路由规范将正确路由用户到正确的视图:
[csharp] routes.MapRoute(null,“”,
new { controller = “Products”, action = “List”, category = (string)null, page = 1 }
);
routes.MapRoute(
null,
“Page{page}”,
new { controller = “Products”, action = “List”, category = (string)null },
new { page = @”d+” }
);
routes.MapRoute(
null,
“{category}”,
new { controller = “Products”, action = “List”, page = 1}
);
routes.MapRoute(
null,
“{category}/Page{page}”,
new { controller = “Products”, action = “List”},
new { page = @”d+” }
); [/csharp]
使用命名的路由机制,避免路由歧义
在使用ASP.NET 路由机制时,必须知道路由机制是如何工作的。否则,会浪费很多时间去跟踪错误的路由。有一个办法是显式命名路由,可缓解这一问题。
如下路由映射定义了命名路由: [csharp] routes.MapRoute(“Default”,“”,
new { controller = “Products”, action = “List”, category = (string)null, page = 1 }
);
routes.MapRoute(
“PageRoute”,
“Page{page}”,
new { controller = “Products”, action = “List”, category = (string)null },
new { page = @”d+” }
); [/csharp]
使用这些路由定义,可创建PageRoute路由链接:
[html] < %= Html.RouteLink(“Next”, “PageRoute”, new RouteValueDictionary( new { page = i + 1 } )); %> [/html]
可扩展性的建议
在ASP.NET MVC 框架中有很多点可扩展,可替换任何核心组件,下面列举了部分:
- routing engine (MvcRouteHandler)
- controller factory (IControllerFactory)
- view engine (IViewEngine)
例如,你可以重写自己的Controller factory,使用IoC容器。另外,你可通过添加定制的行为来扩展框架,在框架中有一些标准的过滤器(filter),如OutputCache、HandleError和Authorize等等。
使用过滤器来添加动作
MVC支持通过attribute添加动作(filter),可在action方法和action结果之前和之后。这些过滤器在请求处理管道允许进行轻量级扩展。
过滤器可应用在action方法上,更改其行为。另外,过滤器也可应用在controller类之上,对controller类中的所有action方法生效。可使用基类base class定义通用的行为方法,并应用过滤器在基类controller上,接着确保其他controller继承该基类。
如你想增加一个功能,记录每一次请求信息,分析HTTP header的问题。如下代码定义了一个继承ActionFilterAttribute的类: [csharp] public class LogHeadersFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { foreach (string header in filterContext.HttpContext.Request.Headers.AllKeys) { Debug.WriteLine(“Header ” + header); Debug.WriteLine(“Value ” + filterContext.HttpContext.Request.Headers.Get(header)); } base.OnActionExecuting(filterContext); } } [/csharp] 为了添加给filter到指定的action方法,可简单将LogHeaderFilter属性添加到Action(或Controller)上面。
可测试性建议
MVC模式的一个主要优点是加强可测试性,保持关注点分离。在ASP.NET MVC 应用程序的Model中,可清晰分离和测试业务逻辑。如你可测试向拍卖网站增加一条拍卖纪录,不必依赖Controller或View。
编写单元测试
ASP.NET MVC 提供了很多工具,开发人员可用来创建可测试的应用程序。此外,也相当比较简单添加第三方的单元测试框架、Mocking框架、或依赖注入容器。ASP.NET MVC 提供了灵活的架构方便单元测试。
安全建议
安全是现代软件开发项目中一个很重要的方面。尽管没有框架提供完美的安全,但有很多地方可做,来保护ASP.NET MVC 应用程序。
安全保护以防攻击
网站安全需要考虑所有web开发人员编写的企业级网站和服务,有许多众所周知的工具,如下所示:
- Cross-site scripting (XSS) attacks
- SQL injection
- Cross-site Request Forgery (XSRF)
- Improperly implementing model binding
(1) 阻止跨站攻击(XSS):
- 使用ValidateInput 属性禁止无效的请求;
- 在所有显示用户输入数据的地方使用Html.Encode,不管是立即呈现或数据将存入数据库,然后显示;
- 在Cookies上设置HttpOnly标识,阻止Javascript读取和发送Cookies;
(2) 阻止SQL注入攻击:
- 总是使用参数化SQL查询;
- 不要传递未经检验的SQL脚本到数据库;
- 使用ORM工具,如Entity Framework,可完全消除在应用程序代码中编写SQL语句;
(3) 阻止跨站请求伪造攻击:
- 在表单中使用Html.AntiForgeryToken 类阻止跨站伪造情况。在接收post请求的Action方法上设置ValidateAntiForgeryToken 属性;
(4) 正确实现model binding:
- 显式指定模型绑定的类成员名称,如[Bind(Include=显式属性名称)];
- 还可创建视图模型(View model),用来接收外部表单输入,视图模型仅包含外部输入的属性;
对用户进行验证和授权保护内容
对于受限的数据视图,通过编写自定义的RoleProvider,或使用Authorize 过滤器属性。
使用<%: %> (.NET 4) 保护,以防XSS攻击
在.NET 4.0 之前,开发人员通过使用如下代码,来确保编码HTML:
<%= Html.Encode(ViewData["name"]) %>
上述代码以防 XSS (cross-site scripting) 攻击。
如果你使用.NET 4,可不必使用上面的语法,简化语法如下:
<%: ViewData["name"] %>
该脚本会自动对字符串进行HTML编码(如有必要)。
本地化和全球化建议
全球化是产品多语言化的进程,本地化是使全球化的产品适应特定的语言和国家。为了开发支持全球化和本地化的web应用程序,至少需要知道一点:不要在View视图中使用硬编码的字符串。
使用ASP.NET 特殊resource文件夹和resource文件
在编写ASP.NET 应用程序时,添加App_GlobalResources存放全球化内容,App_LocalResources存放本地化内容。在这些目录中,根据Controller名称添加资源文件(.resx)。也就是说,如Controller命名为SubmissionPipeline,资源文件应该命名为SubmissionPipeline.resx。
Visual Studio转换这一文本映射类为全局类,可按如下方式调用:
Resources.<resource_filename>.<string_name>
接着在View视图中调用:
<%= Resources.SubmissionPipeline.continueButton %>
当翻译的资源文件存在,每一个资源文件使用如下格式命名:
<filename>.<language>.resx.
如德国版本的资源文件,命名为:SubmissionPipeline.de.resx
性能的建议
网站的性能问题是多方面的,每一个瓶颈都会影响性能:
(1) 数据库
- 低效率查询;
- 错误的索引;
- 非范式设计;
(2) 带宽问题
- 大的文件请求(受单个大的图片文件、CSS / js / html 等等);
- 引用许多其他的资源,如多个script、CSS、或图片文件;
- 慢的连接速度;
(3) 处理能力
- Server: 费时费力的操作;
- Client:低性能的javascript文件;
下面将仅仅关注服务器的处理和请求大小。
考虑使用AJAX的局部页面更新,减小带宽的使用。
缓和服务器处理和请求大小性能问题的一种方法是使用AJAX做局部页面更新。ASP.NET MVC 内置支持AJAX,有助于简化这一模式。性能的改进得益于该模式减少了请求的数量和HTML 片段的大小。
下面示例演示如何使用AJAX 实现局部页面更新:
(1) 选择想更新的HTML局部,并标明ID;
<div> to be updated dynamically </div>
(2) 添加javascript文件,启用AJAX(一般存放在master视图中);
<script src=”<%= Url.Content(“~/Scripts/MicrosoftAjax.js”) %>” type=”text/javascript”></script>
<script src=”<%= Url.Content(“~/Scripts/MicrosoftMvcAjax.js”) %>”></script>
(3) 在想更新的View视图中,添加AJAX链接,引用action方法(范例中action方法命名为RetrieveItems);
<%= Ajax.ActionLink(“Refresh All”, “RetrieveItems”, new { criteria = “all” } , new AjaxOptions { UpdateTargetId = “items” })%>
(4) 实现Controller的action方法,返回Partial 视图;
不要过度使用Session,可使用TempData作为短期存储空间。
在创建web网站时,尝试将对象添加的Session对象中,让对象可用。问题是将这些对象存放在Session中,可导致Server 不得不存放额外的信息,然而这些信息仅仅在页面跳转时有用。正确的方法是将这些页面跳转的临时数据存放在TempData字典中。
假设从POST请求中接收数据,action方法处理POST 过程如下: [csharp] [AcceptVerbs(HttpVerbs.Post)] public ActionResult LogIn(Seller person) { … TempData["name"] = person.Name; return RedirectToAction(“ItemUpload”); } [/csharp] 在上面的示例中,在跳转到ItemUpload action方法之前,seller的名称已存放到TempData 字典中。在ItemUpload action方法中,可从TempData 字典中获取seller的名称,并进一步存放到ViewData字典中,可在View视图中方法。 [csharp] public ActionResult ItemUpload() { string name = TempData["name"] as string; ViewData["name"] = name; return View(); } [/csharp] 对静态页面使用OutputCache 过滤器
对返回的、比较少更新的数据,可使用OutputCache 属性;可应用在首页。可针对HTML和JSON数据类型使用这一技术。在使用OutputCache时,仅需指定缓存名称,不必指定其他内容。如需要更佳调整cache设置,可使用Web.config 文件的output cache部分。
如下所示,OutputCache attribute应用于Dashboard action方法: [csharp] [AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard")] public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page) { … } [/csharp] 在Web.config文件中,设置缓存时间为15秒。
考虑针对长运行请求使用异步controller。
ASP.NET 线程池默认针对每一个CPU有12同时工作线程的限制。当请求数超过server处理这些请求的能力时,这些请求将放置在队列中。如每一个请求都需要大量的时间等待外部的资源,如数据库或大文件操作。在整个等待期间,这些外部请求将阻塞线程。当请求队列很长时(5000个请求等待),server将开始想要503错误(server too busy)。
在ASP.NET 4,同时工作线程默认为5000,可以提高默认的限制。有一个好的办法是让长运行时间的请求异步进行,ASP.NET MVC 支持实现异步controller来实现这一目的。
十二个ASP.NET MVC实践
可以帮助大家更好的进行开发工作。希望对大家有所帮助。
关于Controller的最佳实践
1-删除AccountController
让Demo代码在你的程序中是一个非常不好的做法。请永远不要使用AccountController。
2-隔离外部网络和Controller
如果依赖HttpContext,数据访问类,配置,日志等,则会让程序难以测试,修改或者进一步开发。
3-使用一个IOC容器
使达到第二条最佳实践更加容易,使用IOC容器管理所有外部依赖我使用 Ninject v2,这种IOC容器有很多,如果需要的话,你甚至可以自己实现一个。
4-和“神奇的strings”说不
永远不要使用ViewData[“key”],而要为每一个视图创建一个ViewModel,从而使用强类型的ViewPage。
神奇的Strings是很邪恶的,因为你可能由于错误的拼写而导致视图出错,而强类型的Model不仅可以有智能感知,而且错误是在编译时获取而不是在运行时。
5-创建你自己的“个人惯例”
使用Asp。net MVC作为你个人(或者公司)的参考构架的基础,你还可以使Controller和View继承于你自己的基类而不是默认的基类来让你的惯例更加透彻。
6-注意Verbs
就算不使用最合适的HTTP Verb,最要也要采用PRG模式,(Post-Redirect-Get):使用Get来显示数据,使用Post来修改数据。
关于Model的最佳实践
7–DomainModel != ViewModel
DomainModel代表着相应的域,但ViewModel却是为View的需要而创建。这两者之间或许(一般情况下都)是不同的,此外DomainModel是数据加上行为的组合体,是由复杂的变量类型组成的并且具有层次。而ViewModel只是由一些String等简单变量类型组成。如果想移除冗余并且容易导致出错的ORM代码,可以使用AutoMapper。如果想要了解更多,我推荐阅读:ASP。NET MVC View Model Patterns。
8-为“共享”的数据使用ActionFilter
这是我自己的解决方案,或许需要在未来发帖继续探讨。通常情况下,你都不希望你的Controller获取的数据在几个不同的View之间共享,我的方法则是使用ActionFilter来获取在几个不同View之间共享的数据,然后用合适的View来显示。
关于View的最佳实践
9-不要使用CodeBehind模式
永远不要。
10-尽可能的写HTML代码
我认为Web开发人员必须的习惯于写HTML(或者CSS和JAVASCRIPT)。所以最好少用仅仅用来隐藏HTML代码的HTMLHelper(比如HTML。Submit或者HTML。Button)。这也是我会在未来的帖子里讨论的。
11-如果有if语句,使用HTMLHelper
View必须是哑巴(Controller是瘦子而Model是胖子),如果你发现自己在使用if语句,那就写一个HTMLHelper来隐藏选择条件语句。
12-仔细的选择你的View引擎
默认的引擎室WebFormViewEngine,IMHO并不是最好的引擎,我更倾向于选择Spark ViewEngine,因为对于我来说这个引擎更适合MVC的View。我喜欢的是“dominates the flow and that code should fit seamlessly”对于每一次循环来说IF语句都会被定义在”HTML标签“中。
附录:未来软件发展6大趋势
<非常简短好记!>
一。胖客户瘦服务端
二。操作系统是个网页
三。MVC+REST控制器优化架构
四。RIA富客户方案
五。嵌入插件式风格
六。HTML5---6