内容来自书籍:
Pro ASP.NET Core 6
Develop Cloud-Ready Web Applications Using MVC, Blazor, and Razor Pages (Ninth Edition)Author: Adam Freeman
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的
还可以创建自己的中间件,这是理解 ASP.NET Core 如何工作的有用方法,即使您只使用项目中的标准组件。创建中间件的关键方法是 Use
app.Use(async (context, next) => { if (context.Request.Method == HttpMethods.Get && context.Request.Query["custom"] == "true") { context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Custom Middleware \n"); } await next(); });
Use 方法注册一个中间件组件,该组件通常表示为一个 lambda 函数,该函数在每个请求通过管道时接收它
Lambda 函数的参数是一个 HttpContext 对象和一个函数,它被调用来告诉 ASP.NET Core 将请求传递给管道中的下一个中间件组件
HttpContext 对象描述 HTTP 请求和 HTTP 响应,并提供其他上下文,包括与请求关联的用户的详细信息
HttpContext成员
HttpRequest内部的成员
HttpResponse成员
设置 Content-Type header非常重要,因为它可以防止后续的中间件组件试图设置响应状态码和header。ASP.NET Core 将始终尝试确保发送有效的 HTTP 响应,这可能导致在早期组件已经将内容写入响应主体之后设置响应头或状态代码,从而产生异常(因为在响应主体开始之前必须将头发送到客户端)。
在调用下一个中间件组件时不需要参数,因为 ASP.NET Core 负责为组件提供 HttpContext 对象和它自己的下一个函数,以便它能够处理请求。下一个函数是异步函数,这就是为什么使用了 await 关键字,也是为什么 lambda 函数使用异步关键字定义的原因
(在这个中间件的后面,还有一个MapGet的端点,然后如果这个中间件是写入了响应,然后后面的端点也会写入响应)
public class QueryStringMiddleWare { private RequestDelegate next; public QueryStringMiddleWare(RequestDelegate nextDelegate) { next = nextDelegate; } public async Task Invoke(HttpContext context) { if (context.Request.Method == HttpMethods.Get && context.Request.Query["custom"] == "true") { if (!context.Response.HasStarted) { context.Response.ContentType = "text/plain"; } await context.Response.WriteAsync("Class-based Middleware \n"); } await next(context); } }
中间件类作为构造函数参数接收 RequestDelegate,该参数用于将请求转发到管道中的下一个组件。当接收到一个请求并接收到一个提供对请求和响应的访问的 HttpContext 对象时,使用与 lambda 函数中间件接收到的相同的类,由 ASP.NET Core 调用 Invoke 方法。请求委托返回一个任务,该任务允许它异步工作。
基于类的中间件的一个重要区别是,在调用 RequestDelegate 转发请求时,必须使用 HttpContext 对象作为参数
app.UseMiddleware<QueryStringMiddleWare>();
使用这个中间件
中间件组件可以在调用下一个函数后修改 HTTPResponse 对象
app.Use(async (context, next) => { await next(); await context.Response .WriteAsync($"\nStatus Code: { context.Response.StatusCode}"); });
新的中间件立即调用下一个方法来沿管道传递请求,然后使用 WriteAsync 方法向响应主体添加字符串。这可能看起来是一种奇怪的方法,但是它允许中间件通过在调用下一个函数之前和之后定义语句,在响应沿着请求管道传递之前和之后对响应进行更改
中间件可以在请求传递之前、请求被其他组件处理之后(或者两者兼而有之)进行操作。其结果是,几个中间件组件共同贡献所产生的响应,每个组件都提供响应的某些方面,或者提供稍后在管道中使用的某些特性或数据。
一旦 ASP.NET Core 开始向客户端发送响应,中间件组件就不能更改响应状态代码或头文件。检查表12-6中描述的 HasStarted 属性,以避免异常。
生成完整响应的组件可以选择不调用下一个函数,以便不传递请求。不传递请求的组件会使管道短路
app.Use(async (context, next) => { if (context.Request.Path == "/short") { await context.Response .WriteAsync($"Request Short Circuited"); } else { await next(); } });
即使 URL 具有管道中的下一个组件所期望的查询字符串参数,请求也不会被转发,因此不会使用中间件。但是请注意,管道中的前一个组件已将其消息添加到响应中。这是因为短路只会阻止组件沿着管道进一步使用,而不会影响早期的组件
Map 方法用于创建用于处理特定 URL 请求的管道部分,从而创建一个单独的中间件组件序列
app.Map("/branch", branch => { branch.UseMiddleware<QueryStringMiddleWare>(); branch.Use(async (HttpContext context, Func<Task> next) => { await context.Response.WriteAsync($"Branch Middleware"); }); });
Map 方法的第一个参数指定将用于匹配 URL 的字符串。第二个参数是管道的分支,通过 Use 和 UseMiddleware 方法将中间件组件添加到管道中。
这就是一个终端中间件,而从来不会将请求转发到下一个中间件
branch.Use(async (context, next) => { await context.Response.WriteAsync($"Branch Middleware"); });
ASP.NET Core 支持 Run 方法作为创建终端中间件的一个便利特性,这表明中间件组件不会转发请求,并且已经做出了不调用下一个函数的深思熟虑的决定
branch.Run(async (context) => { await context.Response.WriteAsync($"Branch Middleware"); });
随便创建一个类,用于装载需要的参数
public class MessageOptions { public string CityName { get; set; } = "New York"; public string CountryName{ get; set; } = "USA"; }
然后在项目配置中添加这个类作为配置参数
builder.Services.Configure<MessageOptions>(options => { options.CityName = "Albany"; });
此语句使用 MessageOptions 类创建选项并更改 CityName 属性的值。当应用程序启动时,ASP.NET Core 平台将创建 MessageOptions 类的新实例,并将其传递给作为 Configure 方法的参数提供的函数,从而允许更改默认选项值。
app.MapGet("/location", async (HttpContext context, IOptions<MessageOptions> msgOpts) => { Platform.MessageOptions opts = msgOpts.Value; await context.Response.WriteAsync($"{opts.CityName}, {opts.CountryName}"); });
每个中间件组件决定是否在请求沿管道传递时对其进行操作。有些组件寻找特定的头或查询字符串值,但大多数组件(尤其是终端和短路组件)都试图匹配 URL。
URL 路由通过引入中间件来解决这些问题,中间件负责匹配请求 URL,这样称为端点的组件就可以专注于响应。端点和它们需要的 URL 之间的映射在路由中表示。路由中间件处理 URL,检查路由集,并找到处理请求的端点,这个过程称为路由
路由中间件使用两种不同的方法添加: UseRouting 和 UseEndpoints。UseRouting 方法将负责处理请求的中间件添加到管道中。UseEndpoints 方法用于定义匹配 URL 到端点的路由。URL 使用与请求 URL 路径相比较的模式进行匹配,每个路由在一个 URL 模式和一个端点之间创建一个关系
app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("routing", async context => { await context.Response.WriteAsync("Request Was Routed"); }); });
UseRouting 方法没有参数。UseEndpoints 方法接收一个接受 IEndpointRouteBuilder 对象的函数,并使用该函数使用表的扩展方法创建路由。
分段变量,也称为路由参数,扩大了模式分段匹配的路径分段的范围,允许更灵活的路由。段变量被赋予一个名称,并用大括号({和}字符)表示
app.MapGet("{first}/{second}/{third}", async context => { await context.Response.WriteAsync("Request Was Routed\n"); foreach (var kvp in context.Request.RouteValues) { await context.Response .WriteAsync($"{kvp.Key}: {kvp.Value}\n"); } });
URL 模式{ first }/{ second }/{ Third }匹配其路径包含三个段的 URL,而不管这些段包含什么。当使用段变量时,路由中间件为端点提供它们匹配的 URL 路径段的内容。此内容可通过 HttpRequest.RouteValues 属性获得,该属性返回一个 RouteValuesDictionary 对象
当处理一个请求时,中间件找到所有可以匹配该请求的路由并给每个路由打分,然后选择得分最低的路由来处理该路由。评分过程很复杂,但其效果是最具体的路由接收请求。这意味着文字片段优先于片段变量,有约束的片段变量优先于没有约束的片段变量(约束将在本章后面的“约束片段匹配”章节中描述)。评分系统可以产生令人惊讶的结果,您应该检查以确保您的应用程序支持的 URL 与您期望的路由匹配。/n/n如果两个路由具有相同的分数,这意味着它们同样适合路由请求,那么将抛出异常,表明路由选择不明确
app.MapGet("capital/{country=France}", Capital.Endpoint);
app.MapGet("size/{city?}", Population.Endpoint);
app.MapGet("{first}/{second}/{*catchall}", async context => { await context.Response.WriteAsync("Request Was Routed\n"); foreach (var kvp in context.Request.RouteValues) { await context.Response .WriteAsync($"{kvp.Key}: {kvp.Value}\n"); } });
默认值、可选段和 catchall 段都会增加路由匹配的 URL 范围。约束具有相反的作用并限制匹配。如果端点只能处理特定的段内容,或者如果您想区分匹配不同端点的密切相关的 URL,那么这将非常有用。约束通过冒号(: 字符)和约束类型应用于段变量名之后
app.MapGet("{first:int}/{second:bool}", async context => { await context.Response.WriteAsync("Request Was Routed\n"); foreach (var kvp in context.Request.RouteValues) { await context.Response .WriteAsync($"{kvp.Key}: {kvp.Value}\n"); } });
URL 模式约束
只有在没有其他路由与请求匹配时,后备路由才会将请求定向到端点。后备路由通过确保路由系统始终生成响应来防止请求在请求管道中进一步传递
app.MapFallback(async context => { await context.Response.WriteAsync("Routed to fallback endpoint"); });
尽管路由是在 UseEndpoints 方法中注册的,但路由的选择是在 UseRouting 方法中完成的,并且执行端点以在 UseEndpoints 方法中生成响应。添加到 UseRouting 方法和 UseEndpoints 方法之间的请求管道中的任何中间件组件都可以在生成响应之前看到选择了哪个端点,并相应地改变其行为。
app.Use(async (context, next) => { Endpoint? end = context.GetEndpoint(); if (end != null) { await context.Response .WriteAsync($"{end.DisplayName} Selected \n"); } else { await context.Response.WriteAsync("No Endpoint Selected \n"); } await next(); });
要理解依赖注入,重要的是从它解决的两个问题开始
每个 TextResponseFormatter 对象都有一个包含在发送到浏览器的响应中的计数器,如果我想将同一个计数器合并到其他端点生成的响应中,我需要有一种方法来提供一个单一的 TextResponseFormatter 对象,以便在生成响应的每个点都可以很容易地找到和使用它。
使服务可定位的方法有很多种,但是除了本章的主题外,还有两种主要的方法。第一种方法是创建一个对象,并将其用作构造函数或方法参数,将其传递到需要它的应用程序部分。另一种方法是向服务类添加一个静态属性,该属性提供对共享实例的直接访问,这被称为单例模式,在依赖注入被广泛使用之前,这是一种常见的方法
该单例模式简单易懂,易于使用,但服务定位的知识遍布整个应用程序,所有服务类和服务使用者都需要了解如何访问共享对象。随着新服务的创建,这可能导致单例模式的变化,并在代码中创建许多必须在发生变化时更新的点。这种模式也可能是僵化的,不允许在如何管理服务方面有任何灵活性,因为每个消费者总是共享一个服务对象
虽然定义了一个接口,但是我使用单例模式的方式意味着消费者总是知道他们使用的实现类,因为这个类的静态属性被用来获取共享的对象。如果我想切换到 IResponseFormatter 接口的不同实现,我必须找到服务的每个用法,并用新的实现类替换现有的实现类。还有一些模式可以解决这个问题,比如类型代理模式,其中类通过接口提供对单例对象的访问
public static class TypeBroker { private static IResponseFormatter formatter = new TextResponseFormatter(); public static IResponseFormatter Formatter => formatter; }
Formatter 属性提供对实现 iResponseFormatter 接口的共享服务对象的访问。服务的消费者需要知道 TypeBroker 类负责选择将要使用的实现,但是这种模式意味着服务消费者可以通过接口而不是具体的类工作
这种方法可以通过仅修改 TypeBroker 类来轻松切换到不同的实现类,并防止服务使用者在特定实现上创建依赖项。这也意味着服务类可以专注于它们提供的特性,而不必处理如何定位这些特性。
依赖注入提供了另一种方法来提供服务,清理单例模式和类型代理模式中出现的粗糙边缘,并与其他 ASP.NET 核心功能集成
builder.Services.AddSingleton<IResponseFormatter, HtmlResponseFormatter>(); app.MapGet("middleware/function", async (HttpContext context, IResponseFormatter formatter) => { await formatter.Format(context,"Middleware Function: It is snowing in Chicago"); });
新参数在 IResponseFormatter 接口上声明了一个依赖项,函数被称为依赖于该接口。在调用函数来处理请求之前,要检查函数的参数,检测依赖项,并检查应用程序的服务,以确定是否可以解决依赖项。
对 AddSingleton 方法的调用告诉依赖注入系统,可以使用 HtmlResponseFormatter 对象解决对 IResponseFormatter 接口的依赖。该对象被创建并用作调用处理程序函数的参数。因为解决依赖关系的对象是从使用它的函数外部提供的,所以它被认为是被注入的,这就是为什么这个过程被称为依赖注入
public class WeatherEndpoint { public static async Task Endpoint(HttpContext context) { IResponseFormatter formatter = context.RequestServices.GetRequiredService<IResponseFormatter>(); await formatter.Format(context, "Endpoint Class: It is cloudy in Milan"); } }
HttpContext.RequestServices 属性返回一个实现 IServiceProvider 接口的对象,该接口提供对 Program.cs 文件中配置的服务的访问
使用 HttpContext.RequestServices 方法的缺点是必须为路由到端点的每个请求解析服务。正如您将在本章后面学到的,有一些服务需要这样做,因为它们提供特定于单个请求或响应的特性。IResponseFormatter 服务不是这样,其中一个对象可以用来格式化多个响应。
一种更优雅的方法是在创建端点路由时获取服务,而不是针对每个请求
public static async Task Endpoint(HttpContext context, IResponseFormatter formatter) { await formatter.Format(context, "Endpoint Class: It is cloudy in Milan"); }
public static class EndpointExtensions { public static void MapWeather(this IEndpointRouteBuilder app, string path) { IResponseFormatter formatter = app.ServiceProvider.GetRequiredService<IResponseFormatter>(); app.MapGet(path, context => Platform.WeatherEndpoint .Endpoint(context, formatter)); } }
builder.Services.AddSingleton<IResponseFormatter, HtmlResponseFormatter>();
AddSingleton 方法生成一个服务,该服务在第一次用于解析依赖项时被实例化,然后在每个后续依赖项中重用。这意味着任何对 IResponseFormatter 对象的依赖都将使用相同的 HtmlResponseFormatter 对象来解决。
单例是开始使用服务的好方法,但是有些问题它们不适合,所以 ASP.NET Core 支持scoped服务和transient服务,这些服务为创建来解决依赖关系的对象提供了不同的生命周期
AddTranent 方法与 AddSingleton 方法相反,它为解析的每个依赖项创建一个实现类的新实例。
builder.Services.AddTransient<IResponseFormatter, GuidService>();
只有在解析依赖项时,才会创建新的服务对象,而不是在使用服务时创建新的服务对象。示例应用程序中的组件和端点只有在应用程序启动并执行 Program.cs 文件中的顶级语句时才能解析它们的依赖关系。每个都接收一个单独的服务对象,然后对每个被处理的请求进行重用
如果希望每次都使用新的对象,那么就不要用构造器注入,将注入参数放在方法上,这样每次被调用都会产生新的对象,而且这种方法是针对中间件组件才有的方法
public async Task Invoke(HttpContext context, IResponseFormatter formatter) { if (context.Request.Path == "/middleware/class") { await formatter.Format(context, "Middleware Class: It is raining in London"); } else { await next(context); } }
作用域服务在单例服务和瞬态服务之间取得了平衡。在一个范围内,相关性是用相同的对象解决的。每个 HTTP 请求都会启动一个新的范围,这意味着处理该请求的所有组件将共享一个服务对象。
builder.Services.AddScoped<IResponseFormatter, GuidService>();