ASP.NET Core中有两种路由技术,分别是常规路由和属性路由。
我们先了解什么是路由,当来自浏览器的请求到达应用程序时,MVC中的控制器会处理传入的HTTP请求并响应用户操作,请求URL会被映射到控制器的操作方法上。此映射过程由应用程序中定义的路由规则完成。
比如,当向/Home/Index发出请求时,此URL将映射到HomeController类中的Index()方法,如图所示:
类似的,当向/Home/Details/1发出请求时,此URL将映射到HomeCaontroller类中的Details() 操作方法。URL中的值1自动映射到id,即Details(int id)的参数id。
目前,我们还没有在ASP.NET Core MVC应用中明确定义任何路由规则。由此我们想到的问题是,这个URL(/Home/Index)如何映射到HomeController中的Index()方法。
以下是Startup.cs文件中的Configure()
方法,此方法中的代码设置HTTP请求处理管道。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
在这个方法中,我们调用了UseMvcWithDefaultRoute()扩展方法。正是这种方法将MVC与默认路由添加到应用程序的请求处理管道中。
{Controller=Home}/{action=Index}/{id?}
只需将鼠标悬停在VIsual Studio中的UseMvcWithDefaultRoute()上,就可以看到智能提示的默认路由了。
为了方便读者快速参考,以下是来自Github的代码。注意,UseMvcWithDefaultRoute()方法内部调用了UseMvc()方法,通过它设置默认路由。
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMvc(route=>
{
routes.MapRoute(
name:"default",
template:"{controller=Home}/{action=Index}/{id?}"
);
});
}
默认路由模板规则:{controller=Home}/{action=Index}/{id?},大多数的URL都会按照这个规则进行映射,具体如表所示:
路径段 | 映射信息 |
---|---|
/Home | HomeController类 |
/Details | Details(int id)方法 |
/1 | Details(int id)方法中的id参数 |
请求流程说明如图所示:
请注意,在以下默认路由模板中,我们在id参数后面加一个问号,默认路由模板为{controller=Home}/{action=Index}/{id?}
,问号表示URL中的id参数可选。这意味着通过以下URL,都可以通过路由映射到StudentController类的Details()方法中。
{controller=Home}
中的值Home是Controller的默认值。类似地,{action=Index}
中的值Index是操作方法的默认值。
因此,程序导航到应用程序根目录http://localhost:2051
。因为URL中没有指定控制器名称和操作方法名称,所以将使用路有模板的默认值,路由会将请求映射到HomeController中的Index() 操作方法上。
同理,以下请求URL也将映射到HomeController类的Index()操作方法上。
对于大多数应用程序,默认路由即可满足日常的开发工作需求,代码如下:
public class DepartmentsController:Controller
{
public string List()
{
return "我是Departments控制器的List()方法";
}
public string Details()
{
return "我是Departments控制器的Details()方法";
}
}
/Departments/list
映射到DepartmentsController的List()方法,/Departments/Details映射到DepartmentsController的**Details()**方法。
如果要自定义路径模板并希望更多地控制路径,请使用UseMvc()方法来配置路由,而不是UseMvcWithDefaultRoute()方法,请参考以下代码修改Startup类地路由规则。
app.UseMvc(routes=>
{
routes.MapRoute(
name:"default",
template:"{controller=Home}/{action=Index}/{id?}"
);
});
请参考以下Startup.cs文件中的Configure()方法中的代码。请注意,我们使用的UseMvc()方法不包含默认路由模板,无法进行参数传递。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvc();
//app.UseMvcWithDefaultRoute();
}
这意味着,目前应用程序没有配置任何的路由,当导航到以下任何URL时。我们会看到404错误❌。
使用Route()属性来定义路由,我们可以在Controller类或Controller()操作方法上应用Route()属性。
public class HomeController : Controller
{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
public ViewResult Index()
{
return View();
}
//其他代码
}
在Index()操作方法上指定了3个不同的Route()属性。对于Route()属性的每个实例,我们指定了不同的路有模板。使用这3个路由规则,3个URL中都会访问HomeController的Index()操作方法。
使用这3个路由模板,当遇到以下3个URL访问HomeController的Index()方法时,都会匹配成功并进入方法内。
配置完成后运行项目,我们能正常访问项目首页。
使用传统路由,我们可以将路由参数指定为路由模板的一部分。当然属性路由也可以做同样的事,请看下面的例子。
public class HomeController : Controller
{
private readonly IStudentRepository _studentRepository;
//使用构造函数注入的方式注入IStudentRepository
public HomeController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
[Route("")]
[Route("Home")]
[Route("Home/Index")]
//返回学生的信息
public ViewResult Index()
{
//查询所有的学生信息
IEnumerable<Student> students = _studentRepository.GetAllStudents();
//将学生列表传递到视图
return View(students);
}
[Route("Home/Details/{id}")]
public ViewResult Details(int id)
{
//实例化HomeDetailsViewModel并存储Student详细信息和PageTitle
HomeDetailsViewModel homeDetailsViewModel = new HomeDetailsViewModel()
{
Student = _studentRepository.GetStudent(id),
PageTitle = "学生详情"
};
//将ViewModel对象传递给View()方法
return View(homeDetailsViewModel);
}
}
Details()方法具有id参数,此参数根据指定的id来查询学生的详细信息。
请注意,在路由模板中我们指定了id参数。因此,URL(/Home/Details)将执行Details(int id) 方法,并将值1映射到Details(int id)的id参数。这是通过模型绑定来完成的。
当URL(/Home/Details/1)中具有id路由参数的值时,才执行HomeController的Details(int id)方法。如果URL中不存在id值,那么我们会得到404错误❌。
比如,访问/Home/Details就不会执行Details(int id)方法,而是显示404错误。要使路由参数id可选,只需在末尾添加问号即可。
public class HomeController : Controller
{
private readonly IStudentRepository _studentRepository;
//使用构造函数注入的方式注入IStudentRepository
public HomeController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
[Route("")]
[Route("Home")]
[Route("Home/Index")]
//返回学生的信息
public ViewResult Index()
{
//查询所有的学生信息
IEnumerable<Student> students = _studentRepository.GetAllStudents();
//将学生列表传递到视图
return View(students);
}
[Route("Home/Details/{id?}")]
public ViewResult Details(int? id)
{
//实例化HomeDetailsViewModel并存储Student详细信息和PageTitle
HomeDetailsViewModel homeDetailsViewModel = new HomeDetailsViewModel()
{
//如果id为null,则使用1,否则使用路由属性中传递的值
Student = _studentRepository.GetStudent(id??1),
PageTitle = "学生详情"
};
//将ViewModel对象传递给View()方法
return View(homeDetailsViewModel);
}
}
在属性路由中,控制器和操作方法名称不会影响属性路由名称,它们没有强关联关系。请看下面的示例。
public class WelcomeController:Controller
{
[Route("WC")]
[Route("WC/Index")]
public string Welcome()
{
return "我是Welcome控制器中的welcome()方法";
}
}
由于我们直接在操作方法上指定了路由模板,因此WelcomeController的Welcome()方法对以下两个URL都会执行。
Route()属性也可以应用于Controller类以及各个操作方法中。为了使属性路由代码重复性减弱并提升可维护性,我们可以将Controller上的属性路由与各个操作方法的属性路由相结合。考虑下面的例子。
public class HomeController : Controller
{
private readonly IStudentRepository _studentRepository;
//使用构造函数注入的方式注入IStudentRepository
public HomeController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
[Route("")]
[Route("Home")]
[Route("Home/Index")]
//返回学生的信息
public ViewResult Index()
{
//查询所有的学生信息
IEnumerable<Student> students = _studentRepository.GetAllStudents();
//将学生列表传递到视图
return View(students);
}
//?使路由模板中的id参数为可选,如果要让它为必选,删除?即可
[Route("Home/Details/{id?}")]
public ViewResult Details(int id)
{
//实例化HomeDetailsViewModel并存储Student详细信息和PageTitle
HomeDetailsViewModel homeDetailsViewModel = new HomeDetailsViewModel()
{
Student = _studentRepository.GetStudent(id),
PageTitle = "学生详情"
};
//将ViewModel对象传递给View()方法
return View(homeDetailsViewModel);
}
}
HomeController的Index() 方法匹配以下3中URL。
HomeController的Details(int id) 操作方法匹配以下两种URL路径。
正如读者所看到的,有很多重读的路由名称。我们对代码进行修改并精简,在HomeController类应用Route()属性。如下所示:
[Route("Home")]
public class HomeController : Controller
{
private readonly IStudentRepository _studentRepository;
//使用构造函数注入的方式注入IStudentRepository
public HomeController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
[Route("")]
[Route("Index")]
//返回学生的信息
public ViewResult Index()
{
//查询所有的学生信息
IEnumerable<Student> students = _studentRepository.GetAllStudents();
//将学生列表传递到视图
return View(students);
}
//?使路由模板中的id参数为可选,如果要让它为必选,删除?即可
[Route("Details/{id?}")]
public ViewResult Details(int id)
{
//实例化HomeDetailsViewModel并存储Student详细信息和PageTitle
HomeDetailsViewModel homeDetailsViewModel = new HomeDetailsViewModel()
{
Student = _studentRepository.GetStudent(id),
PageTitle = "学生详情"
};
//将ViewModel对象传递给View()方法
return View(homeDetailsViewModel);
}
}
我们将应用于控制器操作方法的路由模板用到了控制器上。但是,当我们导航到http://localhost:1234
的时候,HomeController的Index()方法将不会被执行,反而出现404页面错误❌。要解决这个问题,请在Index()操作方法中包含以 /
开头的路径模板,如下所示:
[Route("/")]
[Route("")]
[Route("Index")]
//返回学生的信息
public ViewResult Index()
{
//查询所有的学生信息
IEnumerable<Student> students = _studentRepository.GetAllStudents();
//将学生列表传递到视图
return View(students);
}
需要记住的是,如果操作方法上的路与规则以 /
或 ~/
开头,则Controller路由模板不会与操作方法的路由模板组合在一起。
属性路由通过将标记放在方括号中来支持标记替换。标记[controller]
与[action]
将替换为定义路径的控制器名称和操作名称的值。代码如下所示:
[Route("[controller]")]
public class DepartmentsController:Controller
{
[Route("[action]")]
public string List()
{
return "我是Departments控制器的List()方法";
}
[Route("[action]")]
public string Details()
{
return "我是Departments控制器的Details()方法";
}
}
使用[controller]
和[action]
标记,导航到URL路径中的/Departments/List将进入DepartmentsController中执行List()方法。类似地,在URL路径的/Departments/Details将进入DepartmentsController中执行Details()方法。
这是一种非常强大的功能,因为如果要重命名控制器或操作方法名称,我们就不必更改模板路径,如将Departments修改为Some。
要使List()方法成为DepartmentsController的默认路由入口,读者仍可以使用空字符串包含的Route("")
属性,如下所示:
[Route("[controller]")]
public class DepartmentsController:Controller
{
[Route("[action]")]
[Route("")] //使List()成为默认路由入口
public string List()
{
return "我是Departments控制器的List()方法";
}
[Route("[action]")]
public string Details()
{
return "我是Departments控制器的Details()方法";
}
}
我们最好只在控制器上设置一次,而不是在控制器的每个操作方法中都包含[action]
标记,如下所示:
[Route("[controller]/[action]")]
public class DepartmentsController:Controller
{
public string List()
{
return "我是Departments控制器的List()方法";
}
public string Details()
{
return "我是Departments控制器的Details()方法";
}
}
使用属性路由时,属性路由需要在实际使用它们的操作方法上方设置。属性路由比传统路由提供了更大的灵活性。通常情况下,常规路由用于服务HTML页面的控制器,而属性路由则用户服务RESTful API的控制器。
当然,这只是规范和建议,如果读者的应用程序需要有更多的路由灵活性,我们也可以将常规路由与属性路由混合使用。
虽然我们要讨论的核心是EndpointRouting
(终结点路由),它是在ASP.NET Core 2.2中引入的,但在3.0版本中它成为ASP.NET Core的“一等公民”。
首先打开在Startup.cs文件下的ConfigureServices() 方法,修改如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews().AddXmlSerializerFormatters();
services.AddSingleton<IStudentRepository, StudentRepository>();
}
修改后查看Startup类的Configure() 方法,会发现app.UseMvc()中间件弹出了警告。如图所示:
警告告诉我们,UseMvc()中间件不支持Routing Endpoints()中间件,要继续使用UseMvc()中间件需将EnableEndpointRouting的值设置为false,这也是之前我们一直将AddControllersWIthViews服务的值设置为false的原因。
那么现在产生一个问题——EndpointRouting是什么。
在了解EndpointRouting之前,我们先了解一下UseRouting()中间件,它是ASP.NET Core3.0后新增的路由中间价,其主要作用是启用路由 。
UseEndpoints()将会替代原有的路由规则和模板,在以后的开发中,基本都会通过UseEndpoints来设置路由规则,官方将它命名为终结点路由。
但是还有一个显而易见的问题没有解决,为什么要用UseEndpoints()呢,原有的路由中间件不好吗❓答案是原有的路由中间件功能不够丰富。
UseEndpoints是一个可以处理跨不同中间件系统(如MVC、Razor Pages、Blazor、SignalR和gRPC)的路由系统。 通过终结点路由可以使端点相互协作,并使系统比没有相互对话的终端中间件更全面。
当然在此,不会涉及Razor Pages、Blazor、SignalR和gRPC,但是为了项目的长远规划,dotnet开发团队推荐使用终结点路由。
接下来,我们利用UseEndpoints()中间件改造项目,代码如下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
});
//app.UseMvc();
//app.UseMvcWithDefaultRoute();
}