ASP.NET MVC 5 实战:从路由到过滤器
2015.07.20
C#
4 min
1.4k 字
// 目录 · contents
1. 路由系统 2. Model Binding 3. 过滤器管道 4. 依赖注入集成 5. 单元测试 总结
参与了公司第一个 MVC 5 项目后,我把一些核心机制重新梳理了一遍。Web
Forms 时代那种”控件拖放”的开发方式虽然快,但对 HTTP
的控制力很弱,测试也困难。MVC 5
让我第一次真正理解了请求处理流程,这篇文章记录下我的理解。
1. 路由系统
MVC 5 的路由在 RouteConfig.cs 中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class RouteConfig { public static void RegisterRoutes (RouteCollection routes ) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}" ); routes.MapRoute( name: "ProductDetail" , url: "products/{id}" , defaults: new { controller = "Products" , action = "Detail" }, constraints: new { id = @"\d+" } ); routes.MapRoute( name: "Default" , url: "{controller}/{action}/{id}" , defaults: new { controller = "Home" , action = "Index" , id = UrlParameter.Optional } ); } }
Attribute Routing(推荐):
MVC 5 支持直接在 Action 上声明路由,更直观:
1 2 3 4 5 6 7 8 9 10 11 12 [RoutePrefix("api/users" ) ]public class UsersController : Controller { [Route("" ) ] public ActionResult Index () { ... } [Route("{id:int}" ) ] public ActionResult Detail (int id ) { ... } [Route("{id:int}/orders" ) ] public ActionResult Orders (int id ) { ... } }
2. Model Binding
MVC 5 会自动将请求数据绑定到 Action 参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public ActionResult Search (string keyword, int page = 1 , int pageSize = 10 ) { }public ActionResult Create (CreateUserDto dto ) { if (!ModelState.IsValid) { return View(dto); } }public class CreateUserDto { [Required ] [StringLength(50) ] public string Name { get ; set ; } [Required ] [EmailAddress ] public string Email { get ; set ; } [Range(18, 100) ] public int Age { get ; set ; } }
自定义 Model Binder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class IntListModelBinder : IModelBinder { public object BindModel (ControllerContext controllerContext, ModelBindingContext bindingContext ) { var value = bindingContext.ValueProvider .GetValue(bindingContext.ModelName)?.AttemptedValue; if (string .IsNullOrEmpty(value )) return new List<int >(); return value .Split(',' ) .Select(s => int .TryParse(s.Trim(), out int n) ? n : (int ?)null ) .Where(n => n.HasValue) .Select(n => n.Value) .ToList(); } } ModelBinders.Binders.Add(typeof (List<int >), new IntListModelBinder());
3. 过滤器管道
MVC 5 的过滤器按顺序执行,共 4 种类型:
1 Authorization → Action → Result → Exception
自定义授权过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class RequirePermissionAttribute : AuthorizeAttribute { private readonly string _permission; public RequirePermissionAttribute (string permission ) { _permission = permission; } protected override bool AuthorizeCore (HttpContextBase httpContext ) { var user = httpContext.User; if (!user.Identity.IsAuthenticated) return false ; return user.HasClaim("permission" , _permission); } protected override void HandleUnauthorizedRequest (AuthorizationContext filterContext ) { if (filterContext.HttpContext.Request.IsAjaxRequest()) { filterContext.Result = new JsonResult { Data = new { success = false , message = "无权限" }, JsonRequestBehavior = JsonRequestBehavior.AllowGet }; } else { base .HandleUnauthorizedRequest(filterContext); } } } [RequirePermission("user:write" ) ]public ActionResult Create (CreateUserDto dto ) { ... }
Action 过滤器(日志记录):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ActionLogAttribute : ActionFilterAttribute { public override void OnActionExecuting (ActionExecutingContext filterContext ) { var action = filterContext.ActionDescriptor.ActionName; var controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName; var user = filterContext.HttpContext.User.Identity.Name; Log.Info($"[{user} ] {controller} .{action} 开始执行" ); } public override void OnActionExecuted (ActionExecutedContext filterContext ) { if (filterContext.Exception != null ) { Log.Error("Action 执行异常" , filterContext.Exception); } } }
全局异常处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class GlobalExceptionFilter : HandleErrorAttribute { public override void OnException (ExceptionContext filterContext ) { if (filterContext.ExceptionHandled) return ; Log.Error("未处理异常" , filterContext.Exception); if (filterContext.HttpContext.Request.IsAjaxRequest()) { filterContext.Result = new JsonResult { Data = new { success = false , message = "服务器内部错误" } }; } else { filterContext.Result = new ViewResult { ViewName = "Error" }; } filterContext.ExceptionHandled = true ; filterContext.HttpContext.Response.StatusCode = 500 ; } } filters.Add(new GlobalExceptionFilter());
4. 依赖注入集成
MVC 5 本身不内置 DI,需要通过 IDependencyResolver
集成第三方容器(以 Autofac 为例):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class AutofacDependencyResolver : IDependencyResolver { private readonly IContainer _container; public AutofacDependencyResolver (IContainer container ) { _container = container; } public object GetService (Type serviceType ) { return _container.IsRegistered(serviceType) ? _container.Resolve(serviceType) : null ; } public IEnumerable<object > GetServices (Type serviceType ) { var type = typeof (IEnumerable<>).MakeGenericType(serviceType); return _container.IsRegistered(type) ? (IEnumerable<object >)_container.Resolve(type) : Enumerable.Empty<object >(); } }var builder = new ContainerBuilder(); builder.RegisterControllers(Assembly.GetExecutingAssembly()); builder.RegisterType<UserService>().As<IUserService>().InstancePerRequest();var container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
5. 单元测试
MVC 的架构让 Controller 测试变得容易:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 [TestClass ]public class UsersControllerTests { private Mock<IUserService> _mockService; private UsersController _controller; [TestInitialize ] public void Setup () { _mockService = new Mock<IUserService>(); _controller = new UsersController(_mockService.Object); } [TestMethod ] public void Index_ReturnsViewWithUsers () { var users = new List<User> { new User { Name = "Alice" } }; _mockService.Setup(s => s.GetAll()).Returns(users); var result = _controller.Index() as ViewResult; Assert.IsNotNull(result); var model = result.Model as List<User>; Assert.AreEqual(1 , model.Count); } }
总结
ASP.NET MVC 5 相较于 Web Forms 的核心优势在于:
可测试性 :Controller 与 HTTP
解耦,业务逻辑可单独测试
对 HTML/HTTP 的完全控制 :没有 ViewState
和隐藏字段干扰
路由灵活 :Attribute Routing 让 URL
设计更语义化
过滤器管道 :横切关注点(日志、权限、异常)以声明式方式实现
从 Web Forms 迁过来需要转变思维:MVC 要求你理解 HTTP
请求-响应模型,而不是把它抽象成”事件驱动”。这个转变一旦完成,代码质量和可维护性都会显著提升。