ASP.NET Core 中文文档 第四章 MVC(4.2)控制器操作的路由
原文:Routing to Controller Actions
作者:Ryan Nowak、Rick Anderson
翻譯:婁宇(Lyrics)
校對:何鎮汐、姚阿勇(Dr.Yao)
ASP.NET Core MVC 使用路由 中間件 來匹配傳入請求的 URL 并映射到具體的操作。路由通過啟動代碼或者特性定義。路由描述 URL 路徑應該如何匹配到操作。路由也同樣用于生成響應中返回的 URL(用于鏈接)。
這篇文章將解釋 MVC 和路由之間的相互作用,以及典型的 MVC 應用程序如何使用路由特性。查看 路由 獲取更多高級路由信息。
配置路由中間件
在你的 Configure 方法中也許能看到以下代碼:
app.UseMvc(routes => {routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"); });其中對 UseMvc,MapRoute 的調用用來創建單個路由,我們稱之為 default 路由。大部分 MVC 應用程序使用的路由模板類似 default 路由。
路由模板 "{controller=Home}/{action=Index}/{id?}" 能夠匹配路由比如 /Products/Details/5 并會通過標記路徑提取路由值 { controller = Products, action = Details, id = 5 }。MVC 將嘗試定位名為 ProductsController 的控制器并運行操作 Details:
public class ProductsController : Controller {public IActionResult Details(int id) { ... } }注意這個例子,當調用這個操作時,模型綁定會使用 id = 5 的值來將 id 參數設置為 5。查看 模型綁定 獲取更多信息。
使用 default 路由:
路由模板:
- {controller=Home} 定義 Home 作為默認的 controller
- {action=Index} 定義 Index 作為默認的 action
- {id?} 定義 id 為可選項
默認和可選路由參數不需要出現在 URL 路徑,查看 Routing 獲取路由模板語法的詳細描述。
"{controller=Home}/{action=Index}/{id?}" 可以匹配 URL 路徑 / 并產生路由值 { controller = Home, action = Index }。 controller 和 action 使用默認值,因為在 URL 路徑中沒有響應的片段,所以 id 不會產生值。MVC會使用這些路由值選擇 HomeController 和 Index 操作:
public class HomeController : Controller {public IActionResult Index() { ... } }使用這個控制器和路由模板, HomeController.Index 操作會被以下任一 URL 路徑執行:
- /Home/Index/17
- /Home/Index
- /Home
- /
簡便的方法 UseMvcWithDefaultRoute:
app.UseMvcWithDefaultRoute();可以被替換為:
app.UseMvc(routes => {routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"); });UseMvc 和 UseMvcWithDefaultRoute 添加一個 RouterMiddleware 的實例到中間件管道。MVC 不直接與中間件交互,使用路由來處理請求。MVC 通過 MvcRouteHandler 的實例連接到路由。UseMvc 中的代碼類似于下面:
var routes = new RouteBuilder(app);// 添加連接到 MVC,將通過調用 MapRoute 連接。 routes.DefaultHandler = new MvcRouteHandler(...);// 執行回調來注冊路由。 // routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");// 創建路由集合并添加中間件。 app.UseRouter(routes.Build());UseMvc 不會直接定義任何路由,它為 特性 路由在路由集合中添加了一個占位符。UseMvc(Action<IRouteBuilder>) 這個重載讓你添加自己的路由并且也支持特性路由。UseMvc 和它所有的重載都為特性路由添加占位符,不管你如何配置 UseMvc ,特性路由總是可用的。 UseMvcWithDefaultRoute 定義一個默認路由并支持特性路由。
特性路由 章節包含了特性路由的信息。
常規路由
default 路由:
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");是一個 常規路由 的例子。我們將這種風格稱為 常規路由 因為它為 URL 路徑建立了一個 約定 :
- 第一個路徑片段映射控制器名。
- 第二個片段映射操作名。
- 第三個片段是一個可選的 id 用于映射到模型實體。
使用這個 default 路由,URL 路徑 /Products/List 映射到 ProductsController.List 操作,/Blog/Article/17 映射到 BlogController.Article。這個映射只基于控制器名和操作名,與命名空間、源文件位置或者方法參數無關。
小技巧
使用默認路由的常規路由使你可以快速構建應用程序,而不必為你定義的每一個操作想新的 URL 模式。對于 CRUD 風格操作的應用程序,保持訪問控制器 URL 的一致性可以幫助簡化你的代碼并使你的 UI 更加可預測。
警告
id 在路由模板中定義為可選,意味著你可以執行操作且不需要在 URL 中提供 ID。通常在 URL 中忽略 id 會通過模型綁定設置為 0,并且沒有實體會通過在數據庫中匹配 id == 0 被找到。特性路由可以提供細粒度控制使 ID 在某些操作中必傳以及其他操作中不必傳。按照慣例,當可選參數可能出現在正確的用法時,文檔將包括它們,比如 id。
多路由
你可以在 UseMvc 中通過添加 MapRoute 調用來添加多個路由。這樣做讓你可以定義多個約定,或者添加專用于一個特定操作的常規路由,比如:
app.UseMvc(routes => {routes.MapRoute("blog", "blog/{*article}",defaults: new { controller = "Blog", action = "Article" });routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"); }blog 路由在這里是一個 專用常規路由,意味著它使用常規路由系統,但是專用于一個特殊的操作。由于 controller 和 action 不會作為參數出現在路由模板中,它們只能擁有默認值,因此這個路由將總是映射到操作 BlogController.Article。
路由在路由集合中是有序的,并將按照它們添加的順序處理。所以在這個例子中,blog 路由會在 default 路由之前嘗試。
注解
專用常規路由 通常捕捉所有參數,比如使用 {*article} 捕捉 URL 路徑的剩余部分。這樣使得路由 '太貪婪',這意味著它將匹配所有你打算與其他路由規則匹配的路由。把 'greedy' 路由在路由表中置后來解決這個問題。
回退
作為請求處理的一部分,MVC 將驗證路由值是否可以用來在你的應用程序中找到控制器和操作。如果路由值不匹配任何操作,則不會認為路由匹配成功,將會嘗試下一個路由。這叫做 回退,它的目的是簡化路由重疊的情況。
消除歧義操作
當兩個操作通過路由匹配,MVC 必須消除歧義來選擇‘最好的’候選,或者拋出一個異常,比如:
public class ProductsController : Controller {public IActionResult Edit(int id) { ... }[HttpPost]public IActionResult Edit(int id, Product product) { ... } }這個控制器定義兩個操作,它們都會匹配 URL 路徑 /Products/Edit/17 以及路由數據是 { controller = Products, action = Edit, id = 17 }。這是 MVC 控制器中一個典型模式,其中 Edit(int) 顯示編輯產品的表單,Edit(int, Product) 處理提交上來的表單。為了確保這樣可行,MVC 需要在請求是 HTTP POST 時選擇 Edit(int, Product),并在其他 HTTP 謂詞時選擇 Edit(int)。
HttpPostAttribute ( [HttpPost] ) 是 IActionConstraint 的一個實現,它僅允許 HTTP 謂詞為 POST 的請求訪問操作。IActionConstraint 的存在使得 Edit(int, Product) 比 Edit(int) 更好匹配,所以會先首先嘗試 Edit(int, Product)。查看 理解 IActionConstraint 獲取更多信息。
你只會在專門的場景才需要編寫自定義的 IActionConstraint 實現,但重要的是要理解特性的作用,比如 HttpPostAttribute —— 以及為其他 HTTP 謂詞定義的類似的特性。在常規路由中,當操作是“顯示表單 -> 提交表單”工作流時,操作使用相同的名字是很常見的。在回顧 URL 的生成 章節后,這種模式的方便將變得更加明顯。
如果多個路由都匹配,并且 MVC 不能找到‘最好的’路由,將會拋出一個 AmbiguousActionException 異常。
路由名稱
在下面例子中的 "blog" 和 "default" 字符串是路由名稱:
app.UseMvc(routes => {routes.MapRoute("blog", "blog/{*article}",defaults: new { controller = "Blog", action = "Article" });routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"); });路由名稱給予路由一個邏輯名稱,以便被命名的路由可以用于 URL 的生成。這在路由命令可能使 URL 的生成變得復雜時,大大簡化了 URL 的創建。路由名稱在應用程序內必須唯一。
路由名稱對 URL 匹配或者處理請求沒有任何影響;它們只用于 URL 的生成。更多關于 URL 生成的詳細信息參見 路由 ,包括在具體的 MVC 幫助器中生成 URL。
特性路由
特性路由使用一組特性來直接將操作映射到路由模板。在下面的例子中,在 Configure 中使用 app.UseMvc(); 且沒有傳入路由。HomeController 會匹配一組類似于 {controller=Home}/{action=Index}/{id?} 的默認路由 URL:
public class HomeController : Controller {[Route("")][Route("Home")][Route("Home/Index")]public IActionResult Index(){return View();}[Route("Home/About")]public IActionResult About(){return View();}[Route("Home/Contact")]public IActionResult Contact(){return View();} }HomeController.Index() 操作會被 /、/Home 或者 /Home/Index 中任一 URL 路徑執行。
注解
這個例子突出了特性路由與常規路由一個關鍵的不同之處。特性路由需要更多的輸入來指定一個路由;常規路由處理路由更加的簡潔。然而,特性路由允許(也必須)精確控制每個操作的路由模板。
控制器名和操作名在特性路由中是 不會 影響選擇哪個操作的。這個例子會匹配與上個例子相同的 URL。
public class MyDemoController : Controller {[Route("")][Route("Home")][Route("Home/Index")]public IActionResult MyIndex(){return View("Index");}[Route("Home/About")]public IActionResult MyAbout(){return View("About");}[Route("Home/Contact")]public IActionResult MyContact(){return View("Contact");} }注解
上面的路由模板沒有定義針對 action、area 以及 controller 的路由參數。實際上,這些參數不允許出現在特性路由中。因為路由模板已經關聯了一個操作,解析 URL 中的操作名是沒有意義的。
特性路由也可以使用 HTTP[Verb] 特性,比如 HttpPostAttribute。所有這些特性都可以接受路由模板。這個例子展示兩個操作匹配同一個路由模板:
[HttpGet("/products")] public IActionResult ListProducts() {// ... }[HttpPost("/products")] public IActionResult CreateProduct(...) {// ... }對于 /products 這個 URL 路徑來說,ProductsApi.ListProducts 操作會在 HTTP 謂詞是 GET 時執行,ProductsApi.CreateProduct 會在 HTTP 謂詞是 POST 時執行。特性路由首先匹配路由模板集合中通過路由特性定義的 URL。一旦路由模板匹配,IActionConstraint 約束會應用與決定執行哪個操作。
小技巧
當構建一個 REST API,你幾乎不會想在操作方法上使用 [Route(...)]。最好是使用更加具體的 Http*Verb*Attributes 來精確的說明你的 API 支持什么。REST API 的客戶端期望知道映射到具體邏輯操作上的路徑和 HTTP 謂詞。
由于一個特性路由應用于一個特定操作,很容易使參數作為路由模板定義中必須的一部分。在這個例子中,id 是必須作為 URL 路徑中一部分的。
public class ProductsApiController : Controller {[HttpGet("/products/{id}", Name = "Products_List")]public IActionResult GetProduct(int id) { ... } }ProductsApi.GetProducts(int) 操作會被 URL 路徑 /products/3 執行,但不會被 URL 路徑 /products 執行。查看 路由 獲取路由模板以及相關選項的完整描述。
這個路由特性同時也定義了一個 Products_List 的 路由名稱。路由名稱可以用來生成基于特定路由的 URL。路由名稱對路由的 URL 匹配行為沒有影響,只用于 URL 的生成。路由名稱必須在應用程序內唯一。
注解
常規的 默認路由 定義 id 參數作為可選項 ({id?})。而特性路由的這種精確指定 API 的能力更有優勢,比如把 /products 和 /products/5 分配到不同的操作。
聯合路由
為了減少特性路由的重復部分, 控制器上的路由特性會和各個操作上的路由特性進行結合。任何定義在控制器上的路由模板都會作為操作路由模板的前綴。在控制器上放置一個路由特性會使 所有 這個控制器中的操作使用這個特性路由。
[Route("products")] public class ProductsApiController : Controller {[HttpGet]public IActionResult ListProducts() { ... }[HttpGet("{id}")]public ActionResult GetProduct(int id) { ... } }在這個例子中,URL 路徑 /products 會匹配 ProductsApi.ListProducts,URL 路徑 /products/5 會匹配 ProductsApi.GetProduct(int)。兩個操作都只會匹配 GET,因為它們使用 HttpGetAttribute 進行裝飾。
應用到操作上的路由模板以 / 開頭不會聯合控制器上的路由模板。這個例子匹配一組類似 默認路由 的 URL 路徑。
[Route("Home")] public class HomeController : Controller {[Route("")] // Combines to define the route template "Home"[Route("Index")] // Combines to define the route template "Home/Index"[Route("/")] // Does not combine, defines the route template ""public IActionResult Index(){ViewData["Message"] = "Home index";var url = Url.Action("Index", "Home");ViewData["Message"] = "Home index" + "var url = Url.Action; = " + url;return View();}[Route("About")] // Combines to define the route template "Home/About"public IActionResult About(){return View();} }特性路由的順序
與常規路由的根據定義順序來執行相比,特性路由構建一個樹形結構同時匹配所有路由。這種行為看起來像路由條目被放置在一個理想的順序中;最具體的路由會在一般的路由之前執行。
比如,路由 blog/search/{topic} 比 blog/{*article} 更加具體。從邏輯上講,blog/search/{topic} 路由先‘運行’,因為在默認情況下這是唯一明智的排序。使用常規路由,開發者負責按所需的順序放置路由。
特性路由可以配置順序,通過使用所有提供路由特性的框架中的 Order 屬性。路由根據 Order 屬性升序處理。默認的 Order 是 0。使用 Order = -1 設置一個路由,這個路由會在沒有設置 Order 的路由之前運行。使用 Order = 1 會在默認路由排序之后運行。
小技巧
避免依賴于 Order。如果你的 URL 空間需要明確的順序值來使路由正確,那么它可能使客戶端混亂。一般的特性路由會通過 URL 匹配選擇正確的路由。如果 URL 的生成的默認順序不生效,使用路由名作為重載通常比應用 Order 屬性更簡單。
路由模板中的標記替換([controller],[action],[area])
為了方便,特性路由支持 標記替換 ,通過在方括號中封閉一個標記 ([, ]])。標記 [action]、 [area] 以及 [controller] 會被替換成路由中定義的操作所對應的操作名、區域名、控制器名。在這個例子中,操作可以匹配注釋中描述的 URL 路徑。
[Route("[controller]/[action]")] public class ProductsController : Controller {[HttpGet] // Matches '/Products/List'public IActionResult List() {// ...}[HttpGet("{id}")] // Matches '/Products/Edit/{id}'public IActionResult Edit(int id) {// ...} }標記替換發生在構建特性路由的最后一步。上面的例子將與下面的代碼相同:
public class ProductsController : Controller {[HttpGet("[controller]/[action]")] // Matches '/Products/List'public IActionResult List() {// ...}[HttpGet("[controller]/[action]/{id}")] // Matches '/Products/Edit/{id}'public IActionResult Edit(int id) {// ...} }特性路由也可以與繼承相結合。下面與標記替換的集合非常強大。
[Route("api/[controller]")] public abstract class MyBaseController : Controller { ... }public class ProductsController : MyBaseController {[HttpGet] // Matches '/api/Products'public IActionResult List() { ... }[HttpPost("{id}")] // Matches '/api/Products/{id}'public IActionResult Edit(int id) { ... } }標記替換也可以應用于在特性路由中定義路由名稱。[Route("[controller]/[action]", Name="[controller]_[action]")] 將為每一個操作生成一個唯一的路由名稱。
多路由
特性路由支持定義多個路由指向同一個操作。最常見的使用是像下面展示一樣模仿 默認常規路由 :
[Route("[controller]")] public class ProductsController : Controller {[Route("")] // Matches 'Products'[Route("Index")] // Matches 'Products/Index'public IActionResult Index() }放置多個路由特性到控制器上意味著每一個特性都會與每一個操作方法上的路由特性進行結合。
[Route("Store")] [Route("[controller]")] public class ProductsController : Controller {[HttpPost("Buy")] // Matches 'Products/Buy' and 'Store/Buy'[HttpPost("Checkout")] // Matches 'Products/Checkout' and 'Store/Checkout'public IActionResult Buy() }當多個路由特性(IActionConstraint 的實現)放置在一個操作上,每一個操作約束都會與特性定義的路由模板相結合。
[Route("api/[controller]")] public class ProductsController : Controller {[HttpPut("Buy")] // Matches PUT 'api/Products/Buy'[HttpPost("Checkout")] // Matches POST 'api/Products/Checkout'public IActionResult Buy() }小技巧
雖然使用多個路由到操作上看起來很強大,但最好還是保持應用程序的 URL 空間簡單和定義明確。使用多個路由到操作上僅僅在需要的時候,比如支持已經存在的客戶端。
使用 IRouteTemplateProvider 自定義路由特性
框架提供的所有路由特性([Route(...)], [HttpGet(...)] 等等。)都實現了 IRouteTemplateProvider 接口。當應用程序啟動時,MVC 查找控制器類和操作方法上實現了 IRouteTemplateProvider 接口的特性來構建初始路由集合。
你可以通過實現 IRouteTemplateProvider 來定義你自己的路由特性。每個 IRouteTemplateProvider 允許你定義一個包含自定義路由模板,順序以及名稱的單路由:
public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider {public string Template => "api/[controller]";public int? Order { get; set; }public string Name { get; set; } }上面例子中,當 [MyApiController] 特性被應用,會自動設置 Template 為 "api/[controller]"。
使用應用程序模型來自定義特性路由
應用程序模型 是一個在啟動時創建的對象模型,它包含了所有 MVC 用來路由和執行操作的元數據。應用程序模型 包含從路由特性中收集的所有數據(通過 IRouteTemplateProvider)。你可以在啟動時編寫 約定 修改應用程序模型來自定義路由的行為。這個章節展示了一個使用應用程序模型自定義路由的例子。
using Microsoft.AspNetCore.Mvc.ApplicationModels; using System.Linq; using System.Text; public class NamespaceRoutingConvention : IControllerModelConvention {private readonly string _baseNamespace;public NamespaceRoutingConvention(string baseNamespace){_baseNamespace = baseNamespace;}public void Apply(ControllerModel controller){var hasRouteAttributes = controller.Selectors.Any(selector =>selector.AttributeRouteModel != null);if (hasRouteAttributes){// This controller manually defined some routes, so treat this // as an override and not apply the convention here.return;}// Use the namespace and controller name to infer a route for the controller.//// Example://// controller.ControllerTypeInfo -> "My.Application.Admin.UsersController"// baseNamespace -> "My.Application"//// template => "Admin/[controller]"//// This makes your routes roughly line up with the folder structure of your project.//var namespc = controller.ControllerType.Namespace;var template = new StringBuilder();template.Append(namespc, _baseNamespace.Length + 1,namespc.Length - _baseNamespace.Length - 1);template.Replace('.', '/');template.Append("/[controller]");foreach (var selector in controller.Selectors){selector.AttributeRouteModel = new AttributeRouteModel(){Template = template.ToString()};}} }混合路由
MVC 應用程序可以混合使用常規路由和特性路由。對于給瀏覽器處理頁面的控制器,通常使用常規路由;對于提供 REST API 的控制器,通常使用特性路由。
操作在常規路由或者特性路由中二選一。放置一個路由到控制器上或者操作上使操作變為特性路由。定義為特性路由的操作不能通過常規路由訪問,反之亦然。放置在控制器上的 任何 路由特性都會使控制器中的所有操作變為特性路由。
注解
這兩種路由系統的區別是通過 URL 匹配路由模板的過程。在常規路由中,匹配中的路由值被用來在所有常規路由操作的查找表中選擇操作以及控制器。在特性路由中,每個模板已經關聯了一個操作,進一步查找是沒必要的。
URL 的生成
MVC 應用程序可以使用路由 URL 的生成特性來生成 URL 鏈接到操作。生成 URL 消除硬編碼 URL,使你的代碼健壯和易維護。這個章節關注 MVC 提供的 URL 生成特性,并只覆蓋如何生成 URL 的基本知識。查看 路由 獲取 URL 生成的詳細描述。
IUrlHelper 接口是 MVC 與生成 URL 的路由之間基礎設施的基本塊。你可以通過控制器、視圖以及視圖組件中的 Url 屬性找到一個可用的 IUrlHelper 實例。
在這個例子中,IUrlHelper 接口用于 Controller.Url 屬性來生成一個到其他操作的 URL 。
using Microsoft.AspNetCore.Mvc;public class UrlGenerationController : Controller {public IActionResult Source(){// Generates /UrlGeneration/Destinationvar url = Url.Action("Destination");return Content($"Go check out {url}, it's really great.");}public IActionResult Destination(){return View();} }如果應用程序使用默認的常規路由,url 變量的值會是 URL 路徑字符串 /UrlGeneration/Destination。這個 URL 路徑是將路由值與當前請求(環境值)相結合而成的,并將值傳遞給 Url.Action 并替換這些值到路由模板:
ambient values: { controller = "UrlGeneration", action = "Source" } values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" } route template: {controller}/{action}/{id?}result: /UrlGeneration/Destination路由模板中每一個路由參數的值都被匹配名字的值和環境值替換。一個路由參數如果沒有值可以使用默認值,或者該參數是可選的則跳過(就像這個例子中 id 的情況)。任何必須的路由參數沒有相應的值會導致 URL 的生成失敗。如果一個路由中 URL的生成失敗,會嘗試下一個路由,直到所有路由都嘗試完成或者找到匹配的路由。
上面 Url.Action 的例子假設是傳統路由,但是 URL 的生成工作與特性路由類似,盡管概念是不同的。在路由值常規路由中,路由值被用來擴大一個模板,并且關于 controller 和 action 的路由值通常出現在那個模板中 —— 這生效了,因為路由匹配的URL 堅持了一個 約定。在特性路由中,關于 controller 和 action 的路由值不被允許出現在模板中 —— 它們用來查找該使用哪個模板。
這個例子使用特性路由:
// In Startup class public void Configure(IApplicationBuilder app) {app.UseMvc(); } using Microsoft.AspNetCore.Mvc;public class UrlGenerationController : Controller {[HttpGet("")]public IActionResult Source(){var url = Url.Action("Destination"); // Generates /custom/url/to/destinationreturn Content($"Go check out {url}, it's really great.");}[HttpGet("custom/url/to/destination")]public IActionResult Destination() {return View();} }MVC 構建了一個所有特性路由操作的查找表并且會匹配 controller 和 action 值選擇路由模板用于 URL 的生成。在上面的例子中,custom/url/to/destination 被生成了。
通過操作名生成 URL
Url.Action ( IUrlHelper 、 Action)以及所有相關的重載都是基于通過指定控制器名和操作名來指定想要鏈接到的地方的。
注解
當使用 Url.Action,controller 和 action 的當前路由值是為你指定的 —— controller 和 action 的值同時是 環境值 和 值 的一部分。Url.Action 方法總是使用 controller 和 action 的當前值并且生成路由到當前操作的 URL 路徑。
路由嘗試使用環境值中的值來填充信息,以至于在生成 URL 時你不需要提供信息。使用路由如 {a}/{b}/{c}/vt6mr5x 并且環境值 { a = Alice, b = Bob, c = Carol, d = David },路由擁有足夠的信息生成路由而不需要任何額外的值 —— 因為所有的路由參數都有值。如果你添加值 { d = Donovan },那么值 { d = David } 會被忽略,并且生成的 URL 路徑會是 Alice/Bob/Carol/Donovan。
警告
URL 路徑是分層次的。在上面的例子中,如果你添加值 { c = Cheryl },所有的值 { c = Carol, d = David } 會被忽略。在這種情況下,我們不再有 d 的值,且 URL 生成會失敗。你需要指定 c 和 d 所需的值。你可能期望用默認路由 ({controller}/{action}/{id?}) 來解決這個問題 —— 但是你很少會在實踐中遇到這個問題,Url.Action 總會明確地指定 controller 和 action 的值。
Url.Action 較長的重載也采取額外的 路由值 對象來提供除了 controller 和 action 意外的路由參數。你最長看到的是使用 id,比如 Url.Action("Buy", "Products", new { id = 17 })。按照慣例,路由值 通常是一個匿名類的對象,但是它也可以是一個 IDictionary<> 或者一個 普通的 .NET 對象。任何額外的路由值不會匹配放置在查詢字符串中的路由參數。
using Microsoft.AspNetCore.Mvc;public class TestController : Controller {public IActionResult Index(){// Generates /Products/Buy/17?color=redvar url = Url.Action("Buy", "Products", new { id = 17, color = "red" });return Content(url);} }小技巧
為了創建一個絕對 URL,使用一個接受 protocol 的重載: Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme)
通過路由生成 URL
上面的代碼展示了通過傳遞控制器名和操作名創建 URL。IUrlHelper 也提供 Url.RouteUrl 的系列方法。這些方法類似 Url.Action,但是它們不復制 action 和 controller 的當前值到路由值。最常見的是指定一個路由名來使用具體的路由生成 URL,通常 沒有 指定控制器名或者操作名。
using Microsoft.AspNetCore.Mvc;public class UrlGenerationController : Controller {[HttpGet("")]public IActionResult Source(){var url = Url.RouteUrl("Destination_Route"); // Generates /custom/url/to/destinationreturn Content($"See {url}, it's really great.");}[HttpGet("custom/url/to/destination", Name = "Destination_Route")]public IActionResult Destination() {return View();} }在 HTML 中生成URL
IHtmlHelper 提供 HtmlHelper 方法 Html.BeginForm 和 Html.ActionLink 來分別生成 <form> 和 <a> 元素。這些方法使用 Url.Action 方法來生成一個 URL 并且它們接受類似的參數。Url.RouteUrl 相對于 HtmlHelper 的是 Html.BeginRouteForm 和 Html.RouteLink,它們有著類似的功能。查看 :doc:/mvc/views/html-helpers 獲取更多信息。
TagHelper 通過 form 和 <a> TagHelper 生成 URL。這些 都使用了 IUrlHelper 為它們的實現。查看 Working with Forms 獲取更多信息。
內部觀點,IUrlHelper 通過 Url 屬性生成任何不包含上述的特定 URL。
在操作結果中生成 URL
上面的例子展示了在控制器中使用 IUrlHelper,而在控制器中最常見的用法是生成一個 URL 作為操作結果的一部分。
ControllerBase 和 Controller 基類針對引用其他操作的操作結果提供了方便的方法。一個典型的使用是接受用戶輸入后重定向。
public Task<IActionResult> Edit(int id, Customer customer) {if (ModelState.IsValid){// Update DB with new details.return RedirectToAction("Index");} }操作結果工廠方法遵循 IUrlHelper 中類似模式的方法。
專用常規路由的特殊情況
常規路由可以使用一種特殊的路由被稱作 專用常規路由。在下面的例子中,被命名為 blog 的路由是專用常規路由。
app.UseMvc(routes => {routes.MapRoute("blog", "blog/{*article}",defaults: new { controller = "Blog", action = "Article" });routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"); });使用這些路由定義,Url.Action("Index", "Home") 會使用默認路由生成 URL 路徑 /,但是為什么呢?你可能會猜路由值 { controller = Home, action = Index } 會足以用 blog 路由來生成 URL,并且結果會是 /blog?action=Index&controller=Home。
專用常規路由依靠默認路由的一個特殊行為,沒有相應的路由參數,以防止路由生成 URL “太貪婪”。在這種情況下默認的值是 { controller = Blog, action = Article },而不是出現在路由參數中的 controller 或者 action。當路由執行 URL 的生成,提供的值必須匹配默認路由。URL 的生成使用 blog 將失敗,因為值 { controller = Home, action = Index } 不匹配 { controller = Blog, action = Article }。然后路由回退到嘗試 default,并成功。
區域
區域 是一個 MVC 特點,用來組織相關的功能到一個單獨的路由命名空間(針對控制器操作)的組和單獨的文件夾結構中(針對視圖)。使用區域允許一個應用程序擁有多個同名的路由器 —— 只要它們有不同的 區域。使用區域達到通過添加另一個路由參數分層的目的,area 到 controller 以及 action。這個章節將討論如何路由作用于區域 —— 查看 區域 獲取區域如何與視圖配合使用的詳細信息。
下面的例子使用默認常規路由配置 MVC,以及一個命名為 Blog 的 區域路由:
app.UseMvc(routes => {routes.MapAreaRoute("blog_route", "Blog","Manage/{controller}/{action}/{id?}");routes.MapRoute("default_route", "{controller}/{action}/{id?}"); });當匹配 URL 路徑如 /Manage/Users/AddUser 時,第一個路由會產生路由值 { area = Blog, controller = Users, action = AddUser }。area 路由值是通過 area 的默認值產生的,實際上通過 MapAreaRoute 創建路由和下面的方式是相等的:
app.UseMvc(routes => {routes.MapRoute("blog_route", "Manage/{controller}/{action}/{id?}",defaults: new { area = "Blog" }, constraints: new { area = "Blog" });routes.MapRoute("default_route", "{controller}/{action}/{id?}"); });MapAreaRoute 創建一個路由同時使用默認路由和 area 約束,約束使用提供的區域名,在這個例子中是 Blog。默認值保證路由總是處理 { area = Blog, ... },約束要求值 { area = Blog, ... } 來進行 URL 的生成。
小技巧
常規路由是順序依賴。一般來說,區域路由需要被放置在路由表的前面,因為沒有比區域路由更具體的路由了。
使用上述例子,路由值將匹配下面操作:
using Microsoft.AspNetCore.Mvc;namespace MyApp.Namespace1 {[Area("Blog")]public class UsersController : Controller{public IActionResult AddUser(){return View();} } }AreaAttribute 表示控制器屬于一個區域的一部分,我們說,這個控制器是在 Blog 區域。控制器不帶 [Area] 特性則不是任何區域的成員,并且當 area 路由值通過路由提供時 不會 匹配。在下面的例子中,只有第一個列出的控制器可以匹配路由值 { area = Blog, controller = Users, action = AddUser }。
using Microsoft.AspNetCore.Mvc;namespace MyApp.Namespace1 {[Area("Blog")]public class UsersController : Controller{public IActionResult AddUser(){return View();} } } using Microsoft.AspNetCore.Mvc;namespace MyApp.Namespace2 {// Matches { area = Zebra, controller = Users, action = AddUser }[Area("Zebra")]public class UsersController : Controller{public IActionResult AddUser(){return View();} } } using Microsoft.AspNetCore.Mvc;namespace MyApp.Namespace3 {// Matches { area = string.Empty, controller = Users, action = AddUser }// Matches { area = null, controller = Users, action = AddUser }// Matches { controller = Users, action = AddUser }public class UsersController : Controller{public IActionResult AddUser(){return View();}} }注解
為了完整性,將每個控制器的命名空間顯示到這里 —— 否則控制器將會遇到命名沖突并且聲稱一個編譯錯誤。類命名空間不影響 MVC 的路由。
前兩個控制器是區域的成員,并只匹配通過 area 路由值提供的各自的區域名。第三個控制器不是任何區域的成員,只會在路由中沒有 area 值時匹配。
注解
在匹配 no value 方面,缺少 area 值與 area 是 null 或者空字符串是一樣的。
當執行一個區域內的操作時,area 的路由值可作為用于生成 URL 的 環境值。這意味著默認情況下區域針對 URL 的生成有 黏性 ,如下面例子所示。
app.UseMvc(routes => {routes.MapAreaRoute("duck_route", "Duck","Manage/{controller}/{action}/{id?}");routes.MapRoute("default", "Manage/{controller=Home}/{action=Index}/{id?}"); }); using Microsoft.AspNetCore.Mvc;namespace MyApp.Namespace4 {[Area("Duck")]public class UsersController : Controller{public IActionResult GenerateURLInArea(){// Uses the 'ambient' value of areavar url = Url.Action("Index", "Home"); // returns /Managereturn Content(url);}public IActionResult GenerateURLOutsideOfArea(){// Uses the empty value for areavar url = Url.Action("Index", "Home", new { area = "" }); // returns /Manage/Home/Indexreturn Content(url);}} }理解 IActionConstraint
注解
這一節是框架內部的一個深潛和 MVC 如何選擇操作執行。通常一個應用程序不需要自定義 IActionConstraint
你可能已經使用 IActionConstraint 即使你不熟悉這個接口。[HttpGet] 特性以及類似的 [Http-VERB] 特性實現 IActionConstraint 接口以用于限制操作方法的執行。
public class ProductsController : Controller {[HttpGet]public IActionResult Edit() { }public IActionResult Edit(...) { } }假設默認的常規路由,URL 路徑 /Products/Edit 會產生值 { controller = Products, action = Edit },將 同時 匹配這里顯示的兩個操作。在 IActionConstraint 的術語中,我們會說這兩個操作同時被視為候選項 —— 因為它們都匹配路由數據。
當 HttpGetAttribute 執行,它將聲明 Edit() 匹配 GET 并且不匹配其他的 HTTP 謂詞。Edit(...) 操作沒有定義任何約束,所以會匹配任何 HTTP 謂詞。所以假設有一個 POST 操作 —— 只有 Edit(...) 會匹配。但是如果是 GET 兩個操作都會匹配 —— 然而,一個操作使用了 IActionConstraint 總是被認為 更好 與沒有使用的操作。所以因為 Edit() 有 [HttpGet] ,它被視為更加具體,并且在兩個操作都可以匹配時被選中。
從概念上講, IActionConstraint 是 重載 的一種形式,但不是使用相同名稱的重載方法,它是匹配相同 URL 的操作的重載。特性路由也使用 IActionConstraint 并且可能導致不同控制器的操作被視為候選。
實現 IActionConstraint
實現 IActionConstraint 最簡單的方式是創建一個類派生自 System.Attribute 并且將它放置到你的操作和控制器上。MVC 會自動發現任何作為特性被應用的 IActionConstraint。你可以使用應用程序模型來應用約束,并且這可能是最靈活的方法,因為它可以允許你對它們如何被應用進行元編程。
在下面的例子,一個約束選擇一個操作基于一個來自路由數據的 country code 。GitHub 上完整的示例 .
public class CountrySpecificAttribute : Attribute, IActionConstraint {private readonly string _countryCode;public CountrySpecificAttribute(string countryCode){_countryCode = countryCode;}public int Order{get{return 0;}}public bool Accept(ActionConstraintContext context){return string.Equals(context.RouteContext.RouteData.Values["country"].ToString(),_countryCode,StringComparison.OrdinalIgnoreCase);} }你負責實現 Accept 方法并選擇一個 ‘Order’ 用于約束執行。在這個例子中,Accept 方法返回 true 表示當 country 路由值匹配時操作是匹配的。這和 RouteValueAttribute 不同,因為它允許回退到一個非特性操作。這個例子展示了如果你定義一個 en-US 操作,然后國家代碼是 fr-FR 會回退到一個更通用的控制器,這個控制器沒有應用 [CountrySpecific(...)]。
Order 特性決定約束的部分是哪個階段。操作約束基于 Order 在組中運行。比如,所有框架提供的 HTTP 方法特性使用相同 Order 值,所以他們運行在同一階段。你可以擁有許多階段,來實現你所需要的策略。
小技巧
要決定一個 Order 的值,考慮你的約束是否需要在 HTTP 方法之前被應用。數字越低,運行越早。
總結
以上是生活随笔為你收集整理的ASP.NET Core 中文文档 第四章 MVC(4.2)控制器操作的路由的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Properties 配置文件参数 注入
- 下一篇: Node.js 切近实战(八) 之Exc