1. 程式人生 > >ASP.NET Core 中文文件 第四章 MVC(4.2)控制器操作的路由

ASP.NET Core 中文文件 第四章 MVC(4.2)控制器操作的路由

ASP.NET Core MVC 使用路由 中介軟體 來匹配傳入請求的 URL 並對映到具體的操作。路由通過啟動程式碼或者特性定義。路由描述 URL 路徑應該如何匹配到操作。路由也同樣用於生成響應中返回的 URL(用於連結)。

這篇文章將解釋 MVC 和路由之間的相互作用,以及典型的 MVC 應用程式如何使用路由特性。檢視 路由 獲取更多高階路由資訊。

配置路由中介軟體

在你的 Configure 方法中也許能看到以下程式碼:

app.UseMvc(routes =>
{
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

其中對 UseMvcMapRoute 的呼叫用來建立單個路由,我們稱之為 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 路由:

routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

路由模板:

  • {controller=Home} 定義 Home 作為預設的 controller
  • {action=Index} 定義 Index 作為預設的 action
  • {id?} 定義 id 為可選項

預設和可選路由引數不需要出現在 URL 路徑,檢視 Routing 獲取路由模板語法的詳細描述。

"{controller=Home}/{action=Index}/{id?}" 可以匹配 URL 路徑 / 併產生路由值 { controller = Home, action = Index }controlleraction 使用預設值,因為在 URL 路徑中沒有響應的片段,所以 id 不會產生值。MVC會使用這些路由值選擇 HomeControllerIndex 操作:

public class HomeController : Controller
{
  public IActionResult Index() { ... }
}

使用這個控制器和路由模板, HomeController.Index 操作會被以下任一 URL 路徑執行:

  • /Home/Index/17
  • /Home/Index
  • /Home
  • /
app.UseMvcWithDefaultRoute();

可以被替換為:

app.UseMvc(routes =>
{
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

UseMvcUseMvcWithDefaultRoute 新增一個 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 路由在這裡是一個 專用常規路由,意味著它使用常規路由系統,但是專用於一個特殊的操作。由於 controlleraction 不會作為引數出現在路由模板中,它們只能擁有預設值,因此這個路由將總是對映到操作 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");
   }
}

註解
上面的路由模板沒有定義針對 actionarea 以及 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 屬性升序處理。預設的 Order0。使用 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 空間簡單和定義明確。使用多個路由到操作上僅僅在需要的時候,比如支援已經存在的客戶端。

框架提供的所有路由特性([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/Destination
        var 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 的生成工作與特性路由類似,儘管概念是不同的。在路由值常規路由中,路由值被用來擴大一個模板,並且關於 controlleraction 的路由值通常出現在那個模板中 —— 這生效了,因為路由匹配的URL 堅持了一個 約定。在特性路由中,關於 controlleraction 的路由值不被允許出現在模板中 —— 它們用來查詢該使用哪個模板。

這個例子使用特性路由:

// 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/destination
        return Content($"Go check out {url}, it's really great.");
    }

    [HttpGet("custom/url/to/destination")]
    public IActionResult Destination() {
        return View();
    }
}

MVC 構建了一個所有特性路由操作的查詢表並且會匹配 controlleraction 值選擇路由模板用於 URL 的生成。在上面的例子中,custom/url/to/destination 被生成了。

通過操作名生成 URL

Url.Action ( IUrlHelperAction)以及所有相關的過載都是基於通過指定控制器名和操作名來指定想要連結到的地方的。

註解
當使用 Url.Actioncontrolleraction 的當前路由值是為你指定的 —— controlleraction 的值同時是 環境值 的一部分。Url.Action 方法總是使用 controlleraction 的當前值並且生成路由到當前操作的 URL 路徑。

路由嘗試使用環境值中的值來填充資訊,以至於在生成 URL 時你不需要提供資訊。使用路由如 {a}/{b}/{c}/{d} 並且環境值 { 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 生成會失敗。你需要指定 cd 所需的值。你可能期望用預設路由 ({controller}/{action}/{id?}) 來解決這個問題 —— 但是你很少會在實踐中遇到這個問題,Url.Action 總會明確地指定 controlleraction 的值。

Url.Action 較長的過載也採取額外的 路由值 物件來提供除了 controlleraction 意外的路由引數。你最長看到的是使用 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=red
        var 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,但是它們不復制 actioncontroller 的當前值到路由值。最常見的是指定一個路由名來使用具體的路由生成 URL,通常 沒有 指定控制器名或者操作名。

using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route"); // Generates /custom/url/to/destination
        return 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.BeginFormHtml.ActionLink 來分別生成 <form><a> 元素。這些方法使用 Url.Action 方法來生成一個 URL 並且它們接受類似的引數。Url.RouteUrl 相對於 HtmlHelper 的是 Html.BeginRouteFormHtml.RouteLink,它們有著類似的功能。檢視 :doc:/mvc/views/html-helpers 獲取更多資訊。

TagHelper 通過 form<a> TagHelper 生成 URL。這些 都使用了 IUrlHelper 為它們的實現。檢視 Working with Forms 獲取更多資訊。

內部觀點,IUrlHelper 通過 Url 屬性生成任何不包含上述的特定 URL。

在操作結果中生成 URL

上面的例子展示了在控制器中使用 IUrlHelper,而在控制器中最常見的用法是生成一個 URL 作為操作結果的一部分。

ControllerBaseController 基類針對引用其他操作的操作結果提供了方便的方法。一個典型的使用是接受使用者輸入後重定向。

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 特點,用來組織相關的功能到一個單獨的路由名稱空間(針對控制器操作)的組和單獨的資料夾結構中(針對檢視)。使用區域允許一個應用程式擁有多個同名的路由器 —— 只要它們有不同的 區域。使用區域達到通過新增另一個路由引數分層的目的,areacontroller 以及 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 area
            var url = Url.Action("Index", "Home"); 
            // returns /Manage
            return Content(url);
        }

        public IActionResult GenerateURLOutsideOfArea()
        {
            // Uses the empty value for area
            var url = Url.Action("Index", "Home", new { area = "" }); 
            // returns /Manage/Home/Index
            return 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 codeGitHub 上完整的示例 .

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 中文 MVC4.2控制器操作路由

ASP.NET Core MVC 使用路由 中介軟體 來匹配傳入請求的 URL 並對映到具體的操作。路由通過啟動程式碼或者特性定義。路由描述 URL 路徑應該如何匹配到操作。路由也同樣用於生成響應中返回的 URL(用於連結)。 這篇文章將解釋 MVC 和路由之間的相互作用,以及典型的 MVC 應用程式如何使

ASP.NET Core 中文 MVC4.6Areas區域

Areas 是 ASP.NET MVC 用來將相關功能組織成一組單獨名稱空間(路由)和資料夾結構(檢視)的功能。使用 Areas 建立層次結構的路由,是通過新增另一個路由引數 area 到 Controller 和 action。 Areas 提供了一種把大型 ASP.NET Core MVC Web 應用

ASP.NET Core 中文 MVC4.1Controllers, Actions 和 Action Results

Action 和 action result 是開發者使用 ASP.NET MVC 構建應用程式的基礎部分。 什麼是 Controller 在 ASP.NET MVC 中, 控制器( Controller  )用於定義和聚合操作(Action)的一個集合。操作( 或操作方法 )是控制器中處理入站請求的一個方

ASP.NET Core 中文 MVC4.3過濾器

ASP.NET MVC 過濾器 可在執行管道的前後特定階段執行程式碼。過濾器可以配置為全域性有效、僅對控制器有效或是僅對 Action 有效。 過濾器如何工作? 不同的過濾器型別會在執行管道的不同階段執行,因此它們各自有一套適用場景。根據你實際要解決的問題以及在請求管道中執行的位置來選擇建立不同的過濾器。

ASP.NET Core 中文 MVC4.5測試控制器邏輯

ASP.NET MVC 應用程式的控制器應當小巧並專注於使用者介面。涉及了非 UI 事務的大控制器更難於測試和維護。 章節: 為什麼要測試控制器 控制器是所有 ASP.NET Core MVC 應用程式的核心部分。因此,你應當確保它們的行為符合應用的預期。 自動化測試可以為你提供這樣的保障並能夠在進入生

ASP.NET Core 中文 MVC4.4依賴注入和控制器

ASP.NET Core MVC 控制器應通過它們的構造器明確的請求它們的依賴關係。在某些情況下,單個控制器的操作可能需要一個服務,在控制器級別上的請求可能沒有意義。在這種情況下,你也可以選擇將服務作為 action 方法的引數。 章節: 依賴注入 依賴注入(Dependency injection,

ASP.NET Core 中文 MVC3.8檢視中的依賴注入

ASP.NET Core 支援在檢視中使用 依賴注入 。這將有助於提供檢視專用的服務,比如本地化或者僅用於填充檢視元素的資料。你應該儘量保持控制器和檢視間的關注點分離(separation of concerns)。你的檢視所顯示的大部分資料應該從控制器傳入。 章節: 一個簡單的示例 你可以使用 @i

ASP.NET Core 中文 MVC3.7 區域性檢視partial

ASP.NET Core MVC 支援區域性檢視,當你需要在多個不同檢視間重用同一個頁面部件時會顯得特別有用。 什麼是區域性檢視? 區域性檢視是在其它檢視中被渲染的檢視。區域性檢視執行後生成的 HTML 結果會被渲染到呼叫方檢視或父檢視中。跟檢視檔案一樣,區域性檢視檔案也使用 .cshtml 作為副檔名。

ASP.NET Core 中文 MVC3.9檢視元件

章節: 介紹檢視元件 檢視元件是 ASP.NET Core MVC 中的新特性,與區域性檢視相似,但是它們更加的強大。檢視元件不使用模型繫結,只取決於呼叫它時所提供的資料。檢視元件有以下特點: 渲染一個塊,而不是整個響應 在控制器和檢視之間同樣包含了關注點分離和可測試性帶來的好處 可以擁有引數和業務邏

ASP.NET Core 中文 MVC2.3格式化響應資料

ASP.NET Core MVC 內建支援對相應資料(response data)的格式化,用來修正格式或生成客戶端指定的格式。 特定格式的操作結果 某些操作結果(Action result)的型別是指定的特定格式,比如 JsonResult 或 ContentResult。Action 可以返回格式化為

ASP.NET Core 中文 MVC01ASP.NET Core MVC 概覽

ASP.NET Core MVC 是使用模型-檢視-控制器(Model-View-Controller)設計模式構建網頁應用與 API 的豐富的框架。 什麼是 MVC 模式? 模型-檢視-控制器(MVC)架構模式將一個應用區分為三部分主要元件:模型、檢視、與控制器。這種模式有助實現關注分離。使用這種模式,使

ASP.NET Core 中文 測試5.2整合測試

整合測試確保應用程式的元件組裝在一起時正常工作。 ASP.NET Core支援使用單元測試框架和可用於處理沒有網路開銷請求的內建測試的網路主機整合測試。 章節: 整合測試介紹 整合測試驗證應用程式不同的部位是否正確地組裝在一起。不像單元測試,整合測試經常涉及到應用基礎設施,如資料庫,檔案系統,網路資源

ASP.NET Core 中文 原理4路由

路由是用來把請求對映到路由處理程式。應用程式一啟動就配置了路由,並且可以從URL中提取值用於處理請求。它還負責使用 ASP.NET 應用程式中定義的路由來生成連結。 這份文件涵蓋了初級的ASP.NET核心路由。對於 ASP.NET 核心 MVC 路由, 請檢視 Routing to Controller A

ASP.NET Core 中文 原理2中介軟體

章節: 什麼是中介軟體 中介軟體是用於組成應用程式管道來處理請求和響應的元件。管道內的每一個元件都可以選擇是否將請求交給下一個元件、並在管道中呼叫下一個元件之前和之後執行某些操作。請求委託被用來建立請求管道,請求委託處理每一個 HTTP 請求。 請求委託通過使用 IApplicationBuilder

ASP.NET Core 中文 原理6全球化與本地化

使用 ASP.NET Core 建立一個多語言版本的網站有助於你吸引到更多的使用者,ASP.NET Core 提供服務和中介軟體來支援本地化語言和文化。 國際化涉及 全球化 和 本地化。全球化是為了應用程式支援不同文化而設計的。全球化增加了對特定地理區域的語言文字的輸入、顯示和輸出的支援。 本地化是針對一個

ASP.NET Core 中文 原理11在多個環境中工作

ASP.NET Core 介紹了支援在多個環境中管理應用程式行為的改進,如開發(development),預演(staging)和生產(production)。環境變數用來指示應用程式正在執行的環境,允許應用程式適當地配置。 章節: 開發,預演,生產 ASP.NET Core 引用了一個特定的環境變數

ASP.NET Core 中文 原理13管理應用程式狀態

在 ASP.NET Core 中,有多種途徑可以對應用程式的狀態進行管理,取決於檢索狀態的時機和方式。本文簡要介紹幾種可選的方式,並著重介紹為 ASP.NET Core 應用程式安裝並配置會話狀態支援。 應用程式狀態的可選方式 應用程式狀態 指的是用於描述應用程式當前狀況的任意資料。包括全域性的和使用者特

ASP.NET Core 中文 原理1應用程式啟動

ASP.NET Core 為你的應用程式提供了處理每個請求的完整控制。Startup 類是應用程式的入口(entry point),這個類可以設定配置(configuration)並且將應用程式將要使用的服務連線起來。開發人員可以在 Startup 類中配置請求管道,該管道將用於處理應用程式的所有請求。 章

ASP.NET Core 中文 原理16.NET開放Web介面OWIN

ASP.NET Core 支援 OWIN(即 Open Web Server Interface for .NET 的首字母縮寫),OWIN的目標是用於解耦Web Server和Web Application。此外, OWIN為中介軟體定義了一個標準方法用來處理單個請求以及相關聯的響應。ASP.NET Co

ASP.NET Core 中文 原理17為你的伺服器選擇合適版本的.NET框架

ASP.NET Core基於 .NET Core 專案模型,它支援構建能夠執行在 Windows、Mac和 Linux 上的跨平臺應用程式。當您構建一個 .Net Core 專案的時候,您可以選擇一種 .NET框架來構建您的應用程式,.NET Framework (CLR)、 .NET Core (Core