C# · #csharp#aspnet#mvc

ASP.NET MVC 5 实战:从路由到过滤器

2015.07.20 C# 4 min 1.4k
// 目录 · contents

参与了公司第一个 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+" } // 只匹配数字 ID
);

// 默认路由
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
// 简单类型:从路由、QueryString、Form 按顺序查找
public ActionResult Search(string keyword, int page = 1, int pageSize = 10)
{
// GET /search?keyword=mvc&page=2
}

// 复杂类型:递归绑定所有公共属性
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
// 将逗号分隔的字符串绑定为 List<int>
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;

// 从用户 Claims 中检查权限
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>();
}
}

// Global.asax 中配置
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()
{
// Arrange
var users = new List<User> { new User { Name = "Alice" } };
_mockService.Setup(s => s.GetAll()).Returns(users);

// Act
var result = _controller.Index() as ViewResult;

// Assert
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 请求-响应模型,而不是把它抽象成”事件驱动”。这个转变一旦完成,代码质量和可维护性都会显著提升。

作者 · authorzt
发布 · date2015-07-20
篇幅 · length1.4k 字 · 4 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论