本章介绍 Razor Pages,这是 ASP.NET Core 2.0 中引入的一项功能,它提供了一个不使用控制器的简化开发模型。
通过学习本章,我们将能够开发由数据驱动的动态网站。
我们将讨论以下内容:
- 资产搜查令
- 使用页面模型
- 剃刀观点的共性
- 强制执行安全
要实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition
剃须刀页面是在 ASP.NET Core 2.0 中引入的,它们采用了与 ASP.NET Core 其他部分完全不同的方法。Razor 页面不是 MVC 模式,而是自包含的文件,类似于 XAML 控件或 ASP.NET Web 表单,因为它们也可以有代码隐藏文件。不再存在控制器/视图分离,因为 Razor 页面在一个文件中拥有它们所需的所有内容,尽管我们也可以为它们指定一个类。
要使用 Razor 页面,您需要一个兼容的 Visual Studio 版本,从 2017 Update 3 开始,另外还需要安装 ASP.NET Core 2.0 或更高版本:
Razor 页面物理上存储在文件系统中,位于Pages
文件夹下(这是惯例),页面应具有与常规 Razor 视图相同的.cshtml
扩展名。与之不同的是新的@page
指令。显示的代码如下所示:
@page
@model HelloWorldModel
<!DOCTYPE html>
<html>
<head><title>Hello World</title></head>
<body>
<h1>@Html.Raw("Hello, World!")</h1>
</body>
</html>
添加一个@page
指令(最好作为第一行)会自动将.cshtml
文件变成一个页面。不需要引用任何特定的 NuGet 包或执行任何配置,因为它在默认情况下处于启用状态。
访问 Razor 页面非常简单;由于不涉及路由,可以直接调用,不需要.cshtml
分机:
/HelloWorld
/Admin/Settings
唯一的要求是页面位于Pages
根文件夹中的某个位置。Index.cshtml
文件默认为送达,即如果其中一个文件位于Pages\Admin
文件夹内,则无需明确请求即可送达;/Admin
将提供\Pages\Admin\Index.cshtml
文件。
要路由到 Razor 页面,需要使用新的端点路由机制显式启用它:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
您甚至可以使用新的RequireHost
扩展方法来确保只有在使用某些主机头或端口时才能访问 Razor 页面。不要忘记注册 it 所需的服务:
services.AddRazorPages();
Leave the Pages
prefix and the .cshtml
extension out; they cannot be used in the request. Also, Razor pages cannot start with an underscore (_
).
让我们从资产搜索顺序开始,了解 Razor 页面的位置。
Razor Pages 资产(.cshtml
文件)将按以下文件夹和顺序进行搜索:
Pages
中的当前文件夹/Pages/Shared/
/Views/Shared/
这意味着用户请求的视图名称或布局将首先在当前文件夹(根据请求的路径)中查找,然后在/Pages/Shared
文件夹中查找,最后在/Views/Shared
中查找—所有这些都与应用的根文件夹相关。
因此,在学习了基础知识之后,让我们跳转到页面模型,这是一个非常重要的概念。
您可以使用与 Razor 视图完全相同的语法,但还有更多的东西;Razor 页面本身就有一个与之关联的PageModel
类,请注意指向HelloWorldModel
的@model
指令。该类必须继承自PageModel
,在该类中可以定义处理 HTTP 方法的方法,如GET
或POST
。包含页面模型类定义的文件必须与具有.cs
扩展名的 Razor 页面具有相同的物理名称,位于同一文件夹中,并从PageModel
继承。因此,例如,如果前一个文件名为HelloWorld.cshtml
,则其页面模型将放在HelloWorld.cshtml.cs
文件中:
public class HelloWorldModel : PageModel
{
}
如果您不希望指定自定义页面模型类,系统会自动为您提供一个,您仍然可以在.cshtml
文件中直接指定处理程序方法:
@functions
{
public async Task<IActionResult> OnGetAsync()
{
if (!this.User.Identity.IsAuthenticated)
{
return this.RedirectToAction(actionName: "Login",
controllerName: "Account");
}
return this.Page();
}
}
考虑下面的属性,例如,您可以在 AutoT0-派生类中声明:
public string Message { get; set; }
public void OnGet()
{
this.Message = "Hello, World!";
}
然后可以在.cshtml
文件中使用这些选项:
<p>Message: @Model.Message</p>
您甚至可以在此处声明该类:
@page
@model IndexModel
@functions
{
public class IndexModel : PageModel
{
public async Task<IActionResult> OnGetAsync()
{
//whatever
}
}
}
PageModel
类提供以下属性:
-
HttpContext
(HttpContext
:通常的上下文 -
ModelState
(ModelStateDictionary
:模型状态,由所有值提供者填写 -
PageContext
(PageContext
):提供对当前处理程序方法(如果有)的访问,以及值提供程序和查看开始工厂集合 -
Request
(HttpRequest
):与请求对象HttpContext.Request
相同的值 -
Response
(HttpContext.Response
中的HttpResponse
:响应对象 -
RouteData
(RouteData
:路由数据,一般不需要 -
TempData``ITempDataDictionary
:临时数据 -
Url
(IUrlHelper
):用于生成指向路由动作的 URL,例如User
(ClaimsPrincipal
来自HttpContext.User
:当前用户,由使用中的认证机制确定 -
ViewData
(ViewDataDictionary
):视图包,如第 4 章、控制器和动作中介绍的
这是关于页面模型的一般信息,现在让我们详细了解这些特性;首先,让我们看看如何实现页面处理程序。
HTTP 方法处理程序可以有多个签名:
- 名称必须以
On
开头,后跟 HTTP 方法名称(Get
、Post
、Put
、Delete
等)。 - 返回类型必须为
void
或IActionResult
。 - 如果我们要使用异步版本,则该方法必须返回
Task
或Task<IActionResult>
,并可选地应用async
关键字,并且应该以Async
后缀结尾。 - 它们可以采用参数(具有默认值的基本类型或复杂类型),完全不采用参数,也可以采用
IFormCollection
参数。
现在,您可以添加处理请求的方法,可以是同步的,如下所示:
public IActionResult OnGet()
{
if (this.HttpContext.Request.Headers["HTTP-
Referer"].SingleOrDefault().Contains("google.com") == true)
{
//hey, someone found us through Google!
}
return this.Page();
}
或者,它们可以异步处理请求:
public async Task<IActionResult> OnGetAsync()
{
//...
return this.Page();
}
You cannot have both a synchronous and an asynchronous handler method or multiple overloads for the same HTTP verb, as it will result in a runtime error.
您甚至可以拥有不遵循这些模式的自定义处理程序。实现这一目标的几种方法如下:
- 在查询字符串中传递一个
handler
参数,例如?handler=MyHandler
。 - 改为传递路由中的
handler
参数,例如@page "{handler?}"
。 - 在
<form>
、<input>
或<button>
标记中,设置asp-page-handler
属性,例如asp-page-handler="MyHandler"
(这使用标记处理程序功能)。
通过这种方式,您可以使用以下方法:
public async Task<IActionResult> OnPostMyHandlerAsync() { ... }
不管您给它起什么名字,如果它是一个异步处理程序,您总是会有On
前缀和Async
后缀。
如果希望将页面发布到多个处理程序,根据单击的内容,很容易:
<form method="post">
<input type="submit" value="One Handler" asp-page-handler="One" />
<input type="submit" value="Another Handler" asp-page-handler="Two" />
</form>
要使其工作,两个按钮必须位于具有POST
方法的表单中,并且默认标记帮助程序必须在_ViewImports.cshtml
中注册:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
处理程序的名称必须遵循约定。对于本例,您可以按如下方式使用它们:
public void OnPostOne() { ... }
public async Task<IActionResult> OnPostTwoAsync() { ... }
这只是一个示例,它们可以是异步的,也可以不是异步的,还可以返回值。在内部,他们可以通过返回IActionResult
执行重定向等任务:
public async Task<IActionResult> OnPostTwoAsync()
{
return this.RedirectToPage("/Pages/Success");
}
但并非所有的行动结果都有意义;例如,返回ViewResult
没有意义,因为 Razor 页面不会在控制器的上下文中执行。如果不需要,您甚至不需要返回IActionResult
:
public void OnGet()
{
//initialize everything
}
这些可以作为返回IActionResults
的助手,其方式与ControllerBase
和Controller
类基本相同:
-
Challenge
(ChallengeResult
) -
Content
(ContentResult
) -
File
(FileContentResult
、FileStreamResult
、VirtualFileResult
) -
Forbid
(ForbidResult
) -
LocalRedirect
(LocalRedirectResult
) -
LocalRedirectPermanent
(LocalRedirectResult
) -
LocalRedirectPermanentPreserveMethod
(LocalRedirectResult
) -
LocalRedirectPreserveMethod
(LocalRedirectResult
) -
NotFound
(NotFoundResult
、NotFoundObjectResult
) -
Page
(PageResult
) -
PhysicalFile
(PhysicalFileResult
) -
Redirect
(RedirectResult
) -
RedirectPermanent
(RedirectResult
) -
RedirectPermanentPreserveMethod
(RedirectResult
) -
RedirectPreserveMethod
(RedirectResult
) -
RedirectToAction
(RedirectToActionResult
) -
RedirectToActionPermanent
(RedirectToActionResult
) -
RedirectToActionPermanentPreserveMethod
(RedirectToActionResult
) -
RedirectToActionPreserveMethod
(RedirectToActionResult
) -
RedirectToPage
(RedirectToPageResult
) -
RedirectToPagePermanent
(RedirectToPageResult
) -
RedirectToPagePermanentPreserveMethod
(RedirectToPageResult
) -
RedirectToPagePreserveMethod
(RedirectToPageResult
) -
RedirectToRoute
(RedirectToRouteResult
) -
RedirectToRoutePermanent
(RedirectToRouteResult
) -
RedirectToRoutePermanentPreserveMethod
(RedirectToRouteResult
) -
RedirectToRoutePreserveMethod
(RedirectToRouteResult
) -
SignIn
(SignInResult
) -
SignOut
(SignOutResult
) -
StatusCode
(StatusCodeResult
、ObjectResult
) -
Unauthorized
(UnauthorizedResult
)
其中一些方法提供重载,每个方法都可以返回不同的结果类型。
最后,如果需要,可以将参数传递给处理程序:
<input type="submit" value="Third Handler" asp-page-handler="Three" asp-route-foo="bar" />
只需在处理程序上声明一个参数:
public void OnPostThree(string foo)
{
//do something with the value of foo
}
在了解了如何实现页面处理程序之后,现在让我们看看如何将请求绑定到类模型。
如果您在页面模型类(或@functions
块中声明一个属性,并使用[BindProperty]
属性对其进行修饰,它将自动绑定,使用与前一章中所述相同的规则(绑定源提供程序和绑定属性):
[BindProperty]
public Order Order { get; set; }
然后,您将能够访问和更改它的任何属性,可能是在 HTTP 处理程序方法中。您也可以通过BinderType
属性提供自己的活页夹。如果BindProperty
的SupportsGet
属性设置为true
,则BindProperty
也可以绑定GET
调用。
如果愿意,还可以将[BindProperties]
属性应用于整个类,其所有属性将自动绑定:
[BindProperties]
public class Model
{
public int OneProperty { get; set; }
public string AnotherProperty { get; set; }
}
Do notice that properties bound this way will only be so for non-GET
calls (typically POST
) unless you set its SupportsGet
property (both [BindProperty]
and [BindProperties]
have SupportsGet
). It works pretty much the same as [ModelBinder]
, but the latter never binds on GET
requests.
此外,与控制器操作类似,HTTP 处理程序方法中的参数也会自动绑定:
public void OnGet(int? id = null)
{
//?id=1212
}
您可以选择不将模型声明为处理程序方法签名的一部分,而是动态更新它:
public void OnPost()
{
var model = new OrderModel();
this.TryUpdateModel(model);
}
这样做的一个可能原因是,同一个页面处理不同的请求,从而处理不同的模型。
现在我们已经了解了如何将请求转换为类,现在是学习如何验证它的时候了!
模型验证的工作方式与控制器中的工作方式基本相同:
public IActionResult OnPost()
{
var model = new OrderModel();
this.TryUpdateModel(model);
if (this.TryValidateModel(model))
{
return this.RedirectToPage("/Pages/Error");
}
return this.Page();
}
与控制器类似,ModelState
属性还跟踪所有注入值及其验证状态。
所有常用的数据持久化方法也适用于 Razor 页面,因此这里没有什么特别值得一提的。
Razor 页面可以使用与视图相同的布局功能,但建议您将布局页面保留在Views\Shared
文件夹之外,因为这是为视图保留的。
与视图布局一样,也以完全相同的方式支持局部视图。
现在让我们看看如何支持这些区域。
从 ASP.NET Core 2.1 开始,Razor 页面还支持以下区域。区域是一种在应用内部、文件夹中物理分离模块的方法。这仅仅意味着 Razor 页面可以在这些文件夹中寻址,如以下示例所示:
请注意,必须在项目根目录上的Areas
文件夹下创建这些文件夹,如下所示:
在每个命名区域内,我们必须创建一个Pages
文件夹。在它里面,你可以放任何你喜欢的东西,比如.cshtml
文件、_ViewStart.cshml
和许多其他文件。默认情况下,区域处于启用状态。
是时候提一下 Razor 视图和 Razor 页面中存在的特殊文件了。
Razor 页面尊重_ViewStart.cshtml
和_ViewImports.cshtml
文件,并以与常规 Razor 视图相同的方式处理它们,即在实际页面之前调用它们。它们也适用于不同的区域,这意味着您可以拥有不同的文件,每个区域一个。
接下来,让我们讨论过滤器。
Razor 页面可以使用除操作过滤器之外的任何过滤器。这些过滤器不会被触发,因为您没有操作。还有一个新的过滤器IPageFilter
,还有一个异步版本IAsyncPageFilter
。我已经在“使用过滤器”一节中谈到了它们,所以我在这里不再重复。
我们将在下面看到,依赖注入也受到支持。
您可以按照通常的方式将依赖项注入页面模型类的构造函数中:
public class HelloWorldModel : PageModel
{
public HelloWorldModel(IMyService svc)
{
//yes, dependency injection in the constructor also works!
}
}
如果您在自定义页面模型中用[FromServices]
修饰一个属性,它将被接受,并且该属性的值将从依赖项注入框架及其声明的类型中设置。
您也可以使用@inject
指令,就像在 Razor 视图中一样。
现在,我们将了解如何配置特定于 Razor 页面的选项。
AddRazorPagesOptions
扩展方法可以在AddMvc
之后调用,这样我们可以配置 Razor 页面的一些选项:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/Pages";
});
RazorPagesOptions
类提供以下属性:
AllowAreas
(bool
:是否允许区域默认为false
AllowMappingHeadRequestsToGetHandler
(bool
):如果剃须刀页面(或其型号)没有为HEAD
提供处理程序,HEAD
请求是否会变成GET
请求-默认为false
Conventions
(IList<IApplicationModelConvention>
):将在未来的一章中讨论使用此选项的约定RootDirectory
(string
:相对于应用根目录的根目录,通常设置为/Pages
此外,还有一些扩展方法是通过RazorPagesOptions
配置的,基本上增加了一个或多个约定:
AllowAnonymousToFolder
:允许匿名请求特定文件夹下的所有页面AllowAnonymousToPage
:允许对给定页面进行匿名请求AuthorizeFolder
:为特定文件夹下的所有页面定义授权策略(这将在第 11 章、安全中进行更深入的讨论)AuthorizePage
:定义特定页面的授权策略ConfigureFilter
:允许全局过滤器的配置(添加和删除)
请查看以下示例:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToPage("/Pages/HelloWorld");
});
页面路由是一种特殊的配置,我们将在下面看到。
除了直接调用 Razor 页面外,您还可以让它们应答路由。RazorPagesOptions
有一种新的AddPageRoute
扩展方法,您可以利用它向页面添加友好路由:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/Order", "My/Order/{id:int}");
});
有趣的是,我们可以看到 Razor 页面在某种程度上依赖于 MVC 框架。
AddPageRoute
的参数如下:
pageName
(string
):指向的页面名称,以/
开头,不带.cshtml
后缀route
(string
:常规路由,可能有一些路由或查询字符串参数
在视图中,您可以使用HttpContext.RouteData
或HttpContext.Request.Query
访问任何路由或查询字符串参数。
有趣的是,下面是如何将页面(/HelloWorld
设置为默认页面的:
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/HelloWorld", "");
});
此外,通过向page
指令添加路由模板参数,您可以让 Razor 页面监听特定路由:
@page "{id:int}"
在这种情况下,如果调用 Razor 页面时没有使用id
参数,该参数也必须是int
类型,则不会找到该页面,而是返回 HTTP404错误。
接下来,如何在 Razor 页面中强制执行安全规则。
我们可以通过两种方式在 Razor 页面上强制执行安全规则:
- 通过将
[Authorize]
属性应用于页面模型或页面处理程序 - 通过定义约定
让我们从属性方法开始。
整个页面都很简单:
[Authorize]
public class AdminIndexModel: PageModel
{
}
也可以将其用于单个处理程序:
public class AdminIndexModel: PageModel
{
[Authorize]
public void OnGet() { ... }
}
现在,让我们继续讨论公约。
使用AddRazorPagesOptions
扩展方法,我们可以控制如何将安全性应用于一个或多个页面或文件夹。可用的方法如下:
AllowAnonymousToPage
:授予对单个页面的匿名访问权限AllowAnonymousToFolder
:授予对给定文件夹下所有页面的匿名访问权限AuthorizePage
:定义页面的授权策略AuthorizeFolder
:为文件夹下的所有页面定义授权策略
下面是一个例子:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
//adds an AuthorizeAttribute with a named Policy property
options.Conventions.AuthorizePage("/ShoppingBag",
"Authenticated");
//adds an AuthorizeAttribute
options.Conventions.AuthorizeFolder("/Products");
//adds an AllowAnonymousAttribute
options.Conventions.AllowAnonymousToPage("/Login");
options.Conventions.AllowAnonymousToFolder("/Images");
});
在这里,我们确保**"/ShoppingBag"
端点仅适用于"Authenticated"
策略,并确保尝试访问"/Products"
的任何人都需要授权。最后,任何人都可以使用"/Login"
和"/Images"
**URL,包括匿名用户。
现在,我们可以从 XSS 攻击中学到什么?
服务器上的 Razor 页面默认情况下会检查跨站点请求脚本(XSS攻击。如果您想在 Razor 页面中使用 AJAX,请确保在页面中包含防伪令牌,并在每个 AJAX 请求中发送标题,如第 11 章、**安全中的防伪保护部分所述。
*# 总结
首先,在常规视图和 Razor 页面之间进行选择是一个需要提前做出的决定,因为它们太不一样了。拥有控制器和视图对以前使用过 MVC 的人来说可能更具吸引力,我想说这可以带来更好的耦合和组织,但是 Razor 页面非常容易使用,不需要服务器端代码,也不需要重新编译(如果不使用页面模型的话)。
继续使用局部视图和视图布局,因为它们是提高重用性的良好机制。
与控制器操作一样,Razor 页面也存在同样的安全问题。最好选择约定而不是属性,因为我们有一个存储安全信息的中心位置。
在本章中,我们介绍了 ASP.NET Core 2 的新 Razor Pages 功能,该功能虽然与普通视图不同,但共享相当多的功能。它可以用于更简单的解决方案,而不需要控制器和操作的所有麻烦。
在下一章中,我们将了解如何提取有关 ASP.NET Core 中发生的事情的信息。
您现在应该能够回答以下问题:
- Razor 页面是否使用代码隐藏?
- 页面模型的用途是什么?
- 什么是页面处理程序?
- 如何限制匿名用户调用 Razor 页面?
- 我们可以通过哪两种方式将服务注入 Razor 页面?
- 剃须刀页面是否使用页面布局?
- 默认情况下,Razor 页面在哪里提供服务?*