3. 控制器、操作、路由
让我们来探讨一个 ASP.NET MVC 应用程序的架构:
![]() |
在本章中,我们将探讨将请求 [1] 引导至控制器和将处理该请求的操作 [2a] 的过程,这一机制被称为路由。 此外,我们还将介绍操作可能向浏览器返回的各种响应。这些响应可能并非 V 视图。
3.1. ASP.NET MVC 项目的结构
让我们使用 Visual Studio Express 2012 构建第一个 ASP.NET MVC 项目。我们将将其 [1] 添加到上一章中使用的解决方案中:
![]() |
![]() |
- 并将其命名为 [2];
- 并将其命名为 [3, 4],我们选择 ASP.NET MVC 作为基础项目。 该模板提供了一个空的Web应用程序,但包含所有必要的资源(DLL、JavaScript库等)以供开发使用。
生成的项目如 [5] 所示。我们将 [6] 设为解决方案的启动项目:
![]() |
在 [5] 中需注意以下几点:
- 项目架构反映了其模型 MVC:
- C 控制器将放置在 [Controllers] 文件夹中,
- M数据模型将放置在[Models]文件夹中,
- 视图 V 将放置在文件夹 [Views] 中,
![]() |
- 在 [1] 中,文件 [Site.css] 将作为应用程序的默认文件 CSS;
- 在 [2] 中,提供了一系列 JavaScript 库;
- 在 [3] 中,包含三个特殊视图:_ViewStart、_Layout 和 Error。
文件 [_ViewStart] 内容如下:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
- 第 1 行:@ 字符表示视图中包含 C# 代码序列。实际上,可以在视图中嵌入 C# 代码;
- 第 2 行:定义了一个变量 Layout,该变量定义了所有视图的父视图。它对应于经典 ASP.NET 框架的主页面。
文件 [_Layout] 内容如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
@RenderBody()
@Scripts.Render("~/bundles/jquery")
@RenderSection("scripts", required: false)
</body>
</html>
当渲染 [Views] 文件夹中的某个视图时,其主体内容将由上文第 11 行代码生成。这意味着该视图无需包含 <html>、<head>、<body> 标签。这些标签由上述文件提供。目前该文件较为晦涩难懂。让我们将其简化:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Tutoriel ASP.NET MVC</title>
</head>
<body>
<h2>Tutoriel ASP.NET MVC</h2>
@RenderBody()
</body>
</html>
- 第 6 行:所有视图共用的标题;
- 第 9 行:所有视图共用的页眉;
- 第 10 行:当前显示视图的专属内容。
![]() |
- [Web.config] 是 Web 应用程序的配置文件。该文件较为复杂。当在多层架构中使用 [Spring.net] 框架时,通常需要对其进行修改。
- [Global.asax] 包含应用程序启动时执行的代码。通常,该代码会调用应用程序的各个配置文件,包括 [Web.config]。
3.2. URL的默认路由
[Global.asax]的代码目前如下:
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace Exemple_01
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
...
}
}
}
- 第 6 行,即类中的 namespace。它直接取自项目名称,并在项目属性中存在:

![]() |
我们将默认命名空间设置为 [1]。此后,该项目中创建的所有类都将默认使用该命名空间。
让我们回到 [Global.asax] 的代码:
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace Exemple_01
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
...
}
}
}
- 第 9 行:类 [MvcApplication] 继承自类 [HttpApplication]。名称 [MvcApplication] 可以更改;
- 第 11 行:方法 [Application_Start] 是 Web 应用程序启动时执行的方法。该方法仅执行一次。此处用于初始化应用程序。
[Application_Start] 的代码目前如下:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
目前,我们无需完全理解这段代码。第 5-8 行定义了 Web 应用程序支持的路由。让我们回到 ASP.NET MVC 应用程序的架构:
![]() |
我们之前解释过,[Front Controller] 需要将 URL 路由到负责处理它的操作。 路由用于将 URL 模型与操作建立关联。这些路由由项目中 [App_Start] 文件夹内的 [WebApiConfig, FilterConfig, RouteConfig, BundleConfig] 类定义:
![]() |
目前我们仅关注 [RouteConfig] 类:
using System.Web.Mvc;
using System.Web.Routing;
namespace Exemple_01
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
第12至16行定义了应用程序所接受的URL格式。这被称为一条路由。可能存在多条路由(即多种URL格式)。它们通过名称(第13行)相互区分。 路由中URL的格式在第14行定义。在此,URL将包含三个组成部分:
- {controller}:一个从 [Controller] 派生的类名称。系统将在项目的 [Controllers] 文件夹中查找该类。 按惯例,如果 URL 为 /X/Y/Z,则负责处理该 URL 的控制器将是类 XController。 在URL中,控制器名称后会添加后缀Controller;
- {action}:上述指定控制器中方法的名称。该方法将接收随 URL 一起传入的参数并进行处理。该方法可能返回多种结果:
- void:该操作将自行构建返回给客户端浏览器的响应
- String:该操作向客户端返回一个字符串;
- ViewResult:向客户端返回一个视图;
- PartialViewResult:返回部分视图;
- EmptyResult:向客户端发送空响应;
- RedirectResult:要求客户端重定向至 URL
- RedirectToRouteResult:同上,但 URL 由应用程序的路由构建而成;
- JsonResult:发送 JSON 响应
- JavaScriptResult:向客户端返回一段 JavaScript 代码;
- ContentResult:直接向客户端返回 HTML 数据流,不经过视图;
- FileContentResult:向客户端返回一个文件;
- FileStreamResult:同上,但通过其他途径;
- FilePathResult:……
- {id}:一个将传递给操作的参数。为此,该操作必须包含一个名为 id 的参数。
第 15 行定义了当 URL 不符合预期格式 /{controller}/{action}/{id} 时的默认值。它还表明 URL 中的 {id} 参数是可选的。 以下是一组不完整的 URL 示例,以及使用默认值补全后的 URL:
原始的 URL | 已填入默认值的 URL |
/ | /Home/Index |
/Do | /Do/Index |
/Do/Something | /Do/Something |
/Do/Something/4 | /Do/Something/4 |
/Do/Something/x/y/z | URL 未路由 |
3.3. 创建控制器和第一个操作
创建第一个控制器:
![]() |
![]() |
- 在 [1] 中,为控制器命名并添加后缀 [Controller];
- 在 [2] 中,创建一个空的控制器 MVC;
- 在 [3] 中,已创建该控制器。
[FirstController] 的代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Exemple_01.Controllers
{
public class FirstController : Controller
{
//
// GET: /First/
public ActionResult Index()
{
return View();
}
}
}
- 第 7 行:已生成一个默认的 namespace;
- 第 9 行:类 [FirstController] 继承自类 [System.Web.Mvc.Controller];
- 第 14-17 行:默认生成了一个名为 [Index] 的操作。该操作是公共的。这一点很重要,否则将无法找到它。它返回类型 [ActionResult],这是一个抽象类,大多数操作通常返回的结果都从该类派生而来。 这是一个“万能”类型,可以通过将其替换为实际返回类型的名称来明确指定;
- 第 16 行:该方法不执行任何操作。它仅返回一个视图,即类型 [ViewResult]。视图名称未指定。 在这种情况下,框架会在 [Views / First] 文件夹中查找一个与操作名称相同的视图,即此处的 [Index.cshtml]。
让我们创建视图 [Index.cshtml]:
![]() |
- 在 [1] 中,右键单击操作代码并选择 [Ajouter une vue] 选项;
- 在 [2] 中,向导会建议创建一个与操作名称相同的视图。这正是我们此处所需;
- 在 [3] 中,默认建议使用母版页 [_Layout.cshtml];
- 在 [4] 中,确认后,向导会在 [Views] 文件夹的子文件夹中创建一个以控制器名称(First)命名的视图。
为视图 [Index] 生成的代码如下:
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
- 第 1-3 行:定义变量的 C# 代码;
- 第 5 行:一个 HTML 标签。
将上述所有代码替换为以下代码:
<strong>Vue [Index]...</strong>
总结一下:
- 我们有一个名为 [First] 的 C 控制器;
- 有一个名为 [Index] 的操作,该操作请求显示一个名为 [Index] 的视图;
- 我们有一个名为 [Index] 的视图。
我们可以通过两种方式调用操作 [Index]:
- /First/Index;
- /First,因为 [Index] 也是路由中的默认操作。
运行应用程序(CTRL-F5)。我们将看到以下页面:
1

在 [1] 中,请求的 URL 为 http://localhost:49302。该请求中未包含路径。我们知道路由器期望接收形式为 /{controller}/{action}/{id} 的 URL。由于缺少这些元素,系统将使用默认值。URL 变为 http://localhost:49302/Home/Index。[Home] 控制器不存在。因此 URL 被拒绝。
现在让我们尝试直接在浏览器中输入 URL http://localhost:49302/First/Index:
![]() |
上文中的页面是由控制器 [First] 的操作 [Index] 生成的。该操作生成的页面是视图 [Index],其代码如下:
<strong>Vue [Index]...</strong>
它生成了 [1] 部分。而 [2] 部分则源自我们稍早前定义的主页面 [_Layout]:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Tutoriel ASP.NET MVC</title>
</head>
<body>
<h2>Tutoriel ASP.NET MVC</h2>
@RenderBody()
</body>
</html>
第 9 行生成了页面中的 [2] 部分。而 [Index] 视图仅在第 10 行中出现。
如果查看接收到的页面源代码,可以发现页面 [Index] 确实被包含(如下文第 10 行所示)在页面 [Layout] 中:
现在我们来测试 URL 和 [/First]:
![]() |
URL [/First] 文件不完整。已使用路由的默认值对其进行补全,并更名为 [/First/Index]。因此,我们得到了与之前相同的结果。
3.4. 操作结果类型为 [ContentResult] - 1
在控制器中创建一个新操作 [First]:
using System.Text;
using System.Web.Mvc;
namespace Exemple_01.Controllers
{
public class FirstController : Controller
{
// 首页
public ViewResult Index()
{
return View();
}
// Action01
public ContentResult Action01()
{
return Content("<h1>Action [Action01]</h1>", "text/plain", Encoding.UTF8);
}
}
}
新操作定义在第 14-18 行。它仅通过 [Controller] 类(第 6 行)中的 [Content] 方法(第 16 行)返回一个字符串。该方法的参数为:
- 响应字符串;
- 发送文本类型的标识符: "text/plain"、"text/html"、"text/xml" 等。该标识符称为 MIME 类型(http://fr.wikipedia.org/wiki/Type_MIME);
- 第三个参数用于指定文本所使用的编码类型。
与其使用抽象类型 [ActionResult],我们的方法会明确指定实际渲染的类型(第 9 行和第 14 行)。
让我们调用 URL [/First/Action01]。结果得到以下页面:
![]() |
让我们查看收到的页面的源代码:
浏览器仅接收到了该操作发送的文本,别无其他。当需要向Web服务器请求纯数据,而不带HTML等包装时,此模式非常有用。 需要注意的是,浏览器并未解析<h1>标签。为了解原因,让我们在Chrome中查看数据交互:
浏览器发送了以下头部信息:
服务器返回了以下标头:
- 第 3 行:确定文档的类型。我们可以看到这里设置的属性与 [Action01] 方法中设定的属性一致。正是因为我们告知服务器文档类型为 "text/plain" 而非 "text/html",浏览器才没有解析接收到的文档中包含的 <h1> 标签。
3.5. 操作结果类型为 [ContentResult] - 2
考虑以下第三个操作:
using System.Text;
using System.Web.Mvc;
namespace Exemple_01.Controllers
{
public class FirstController : Controller
{
...
// 操作02
public ContentResult Action02()
{
string data = "<action><name>Action02</name><description>renvoie un texte XML</description></action>";
return Content(data, "text/xml", Encoding.UTF8);
}
}
}
- 第 12 行:定义文本 XML;
- 第 13 行:将其发送至浏览器,并明确标注为 XML,类型为 MIME "text/xml"。
在浏览器中,将显示以下页面:
![]() |
让我们在Chrome中查看服务器返回的HTTP响应:
- 第 3 行:确定文档的类型。这里包含在 [Action02] 方法中设置的属性;
- 第12行:服务器发送的文档大小(以字节为单位)。
服务器发送的文档如下(Chrome截图):
![]() |
3.6. 返回类型为 [JsonResult] 的操作
在控制器中添加以下操作:[First]:
// 操作03
public JsonResult Action03()
{
dynamic personne = new ExpandoObject();
personne.nom = "someone";
personne.age = 20;
return Json(personne,JsonRequestBehavior.AllowGet);
}
- 第 4 行:一个类型为 dynamic 的变量。在运行时,可以自由地向此类变量添加属性。属性在初始化时同时创建;
- 第 5-6 行:初始化两个属性 nom 和 age;
- 第7行:返回对象的JSON(JavaScript对象表示法)表示形式。 JSON 用于将对象序列化为字符串,反之亦可将字符串反序列化为对象。它是 XML 序列化/反序列化的替代方案;
- 第 2 行:该操作返回 [JsonResult] 类型。此类型仅可作为 POST 请求的返回值。 若需将其作为 GET 方法的返回值,则需在 Json 类构造函数的第二个参数(第 7 行)中传入 JsonRequestBehavior.AllowGet 值。
当请求 URL [/First/Action03] 时,浏览器会显示以下内容:
![]() |
服务器返回的响应如下:
- 第 3 行:说明发送的文档是 JSON;
- 第10行:发送的文档大小为58字节。内容如下:
动态元素 [personne] 在 JSON 中被视为一个字典数组,其中每个字典:
- 对应变量 [personne] 中的一个字段;
- 包含两个键“Key”和“Value”。“Key”的关联值为字段名称,“Value”的值为字段的实际值。
3.7. 返回类型为 [string] 的操作
在控制器 [First] 中添加以下操作:
// 操作04
public string Action04()
{
return "<h3>Contrôleur=First, Action=Action04</h3>";
}
当我们在 Chrome 中请求 URL [/First/Action04] 时,会得到以下响应:
![]() |
可见 <h3> 标签已被解析。让我们看看服务器返回的 HTTP 响应:
以及以下文档:
从第3行可以看出,服务器表示发送的是HTML格式的文本。 这就是为什么浏览器会解析 <h3> 标签。因此,当需要发送纯文本时,最好返回 [ContentResult] 而不是 [string]。 因为 [ContentResult] 确实允许我们指定“text/plain”类型(即 MIME),以此表明发送的是未格式化的文本,因此不会被浏览器进行解析。
3.8. 返回类型为 [EmptyResult] 的操作
假设有一个新的操作如下:
// 操作05
public EmptyResult Action05()
{
return new EmptyResult();
}
该操作仅返回类型为 [EmptyResult]。在此情况下,服务器会向客户端发送空响应,如其响应 HTTP 所示:
- 第 9 行:服务器告知客户端,它将发送一个空文档。
3.9. 返回类型为 [RedirectResult] 的操作 - 1
假设存在以下新操作:
// 操作06
public RedirectResult Action06()
{
return new RedirectResult("/First/Action05");
}
该操作返回类型为 [RedirectResult]。此类型用于向客户端发送重定向指令,指向构造函数参数 URL(第 4 行)。 随后,客户端将向 [/First/Action05] 发出新的 GET 请求。因此,客户端总共发出了两次请求。
浏览器显示第二次请求的结果:
![]() |
让我们在 Chrome 中查看服务器的响应 HTTP:
![]() |
上图显示了浏览器的两个请求。让我们来分析第一个请求 [Action06]。服务器的响应 HTTP 如下:
- 第 1 行:服务器返回了 302 Found 状态码。此前返回的是 200 OK,表示已找到请求的文档。302 状态码表明需要进行重定向。 重定向地址由第4行给出。这里显示的正是我们在操作代码中指定的重定向地址;
- 第11行:服务器通过响应HTTP表明,其发送的是132字节(第11行)的text/html类型文档(第3行)。 当我们在Chrome中查看对请求[Action06]的响应时,正如预期的那样,响应内容为空。这可能有其原因,但我尚不清楚。
由于重定向,浏览器会针对上文第4行中指定的URL发起新的请求GET,如下文Chrome第1行所示:
3.10. 操作结果类型为 [RedirectResult] - 2
即以下新操作:
// Action07
public RedirectResult Action07()
{
return new RedirectResult("/First/Action05",true);
}
在第 4 行,我们在 [RedirectResult] 类型的构造函数中添加了第二个参数。这是一个布尔值,默认值为 false。 当将其设为 true 时,会修改发送给客户端的响应 HTTP。响应变为:
因此,发送给客户端的响应代码现在是 301 Moved Permanently。重定向过程与之前相同,但会明确标注此次重定向是永久性的。这使得搜索引擎能够在其结果中将旧的 URL 替换为新的。
3.11. 操作结果为 [RedirectToRouteResult]
假设新的操作如下:
// Action08
public RedirectToRouteResult Action08()
{
return new RedirectToRouteResult("Default",new RouteValueDictionary(new {controller="First",action="Action05"}));
}
- 第 2 行:该操作返回类型 [RedirectToRouteResult]。该类型可将客户端重定向至指定的 URL,其指定方式不再像之前那样使用字符串,而是通过路由。
路由在 [App_Start/RouteConfig] 中定义。目前仅有一个路由:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
- 第 4 行,要求客户端使用 controller=First 和 action=Action05 变量重定向至名为 [Default] 的路由。 路由系统随后将生成重定向路由 URL 至 /First/Action05。服务器响应 HTTP 即显示了这一点:
- 第 1 行:重定向;
- 第4行:由URL路由系统生成的重定向地址。
3.12. 操作结果类型为 [void]
假设以下为新操作:
// Action09
public void Action09()
{
string nom = Request.QueryString["nom"] ?? "inconnu";
Response.AddHeader("Content-Type", "text/plain");
Response.Write(string.Format("<h3>Action09</h3>nom={0}", nom));
}
- 第2行:该操作不返回任何结果。它会直接在发送给客户端的响应流中写入内容;
- 第 4 行:从请求中获取名为 [nom] 的参数(如有)。 该参数可通过控制器 [Controller] 的属性 [Request] 访问,而控制器 [First] 继承了该控制器。 以 [/First/Action09?nom=quelquechose] 形式传递的参数 [nom],可在 Request.QueryString["nom"] 中使用。第 4 行的语法等同于:
- 第5行:发送给客户端的响应可通过控制器[Controller]的属性[Response]访问,而控制器[First]继承了该属性;
- 第5行:设置HTTP [Content-Type]标头,该标头指示服务器即将发送给客户端的文档类型。此处的“text/plain”表示该文档为纯文本,浏览器无需对其进行解析;
- 第6行:在响应流中写入一个字符串。 其中包含 HTML 标签,这些标签不应被浏览器解析,因为浏览器此前已收到 HTTP [Content-Type : text/plain"] 头部。这就是我们要验证的内容。
现在编译项目,并请求 URL [/First/Action09?nom=someone ][1],然后请求URL [/First/Action09 ] [2]:
![]() |
现在让我们在Chrome中查看服务器返回的HTTP响应:
- 第 3 行:我们看到了在操作代码中自行设定的 HTTP 头部。
3.13. 第二个控制器
在项目中创建第二个控制器。我们将遵循第 40 页第 3.1 节中描述的方法。将其命名为 [Second]。
![]() |
其生成的代码如下:
namespace Exemple_01.Controllers
{
public class SecondController : Controller
{
//
// GET: /Second/
public ActionResult Index()
{
return View();
}
}
}
将其修改如下:
using System.Text;
using System.Web.Mvc;
namespace Exemple_01.Controllers
{
public class SecondController : Controller
{
// /Second/Action01
public ContentResult Action01()
{
return Content("Contrôleur=Second, Action=Action01", "text/plain", Encoding.UTF8);
}
}
}
然后使用浏览器请求 URL [/Second/Action01]。我们得到以下响应:
![]() |
该 URL 是通过 HTTP GET 命令请求的,如 Chrome 中该请求的日志 HTTP 所示:
URL 也可以通过 HTTP 和 POST 命令进行请求。为了演示这一点,我们再次使用 [Advanced Rest Client] 应用程序:
![]() |
- 在 [1] 中,启动应用程序(在 Chrome 新标签页的 [Applications] 标签页中);
- 在 [2] 中,选择 [Request] 选项;
- 在 [3] 中,指定所需的 URL;
- 在 [4] 中,指定 URL 必须与 POST 一起请求;
通过 (CTRL-I) 启用 Chrome 日志,以获取服务器的 HTTP 响应。 当执行前一个请求 [Send] 时,HTTP 的交互内容如下:
浏览器发送以下请求:
- 第 1 行:确实请求了 URL,并附带 POST;
- 第 4 行:POST 元素的字节大小。此处没有。
服务器返回的 HTTP 响应如下:
- 第3行:服务器发送了一个未格式化的纯文本文档;
- 第12行:共148个字符。
发送的文档如下:
![]() |
得到的文档与 GET 相同。
3.14. 按属性过滤的操作
创建以下新操作:
// /Second/Action02
[HttpPost]
public ContentResult Action02()
{
return Content("Contrôleur=Second, Action=Action02", "text/plain", Encoding.UTF8);
}
操作 [Action02] 与操作 [Action01] 类似,但指定其仅可通过命令 HTTP POST 访问(第 2 行)。 还可以使用其他属性:
HttpGet | 仅适用于命令 GET |
HttpHead | 仅适用于命令 HEAD |
HttpOptions | 仅服务于订单 OPTIONS |
HttpPut | 仅服务于订单 PUT |
HttpDelete | 仅服务于命令 DELETE |
让我们直接在浏览器中请求 URL [/Second/Action02]。随后,系统会通过 GET 进行请求。浏览器随后显示以下响应:
![]() |
服务器的响应 HTTP 如下:
- 第 1 行:状态码 HTTP 404 Not Found 表示服务器未找到所请求的文档。 在此,操作 [Action02] 无法处理请求 GET,因为它仅处理命令 POST;
- 第 3 行:返回文档的大小。这是浏览器显示的页面:
![]() |
3.15. 获取路线中的元素
在前两个操作中,我们写的是类似这样的代码:
public ContentResult Action02()
{
return Content("Contrôleur=Second, Action=Action02", "text/plain", Encoding.UTF8);
}
控制器和操作的名称在代码中是硬编码的。如果更改这些名称,代码就会失效。可以通过以下方式访问控制器和操作:
// /Second/Action03
public ContentResult Action03()
{
string texte = string.Format("Contrôleur={0}, Action={1}", RouteData.Values["controller"], RouteData.Values["action"]);
return Content(texte, "text/plain", Encoding.UTF8);
}
在 [App_Start/RouteConfig] 中定义的路由如下:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
第3行,路由的三个元素可通过 RouteData.Values["élément"] 获取,其中元素位于 [controller, action, id] 中。
现在获取 URL 和 [http://localhost:49302/Second/Action03]:
![]() |
我们成功获取了控制器名称和操作名称。




























