为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate
毫不夸张地说,路由是ASP.NET Core最为核心的部分。路由的本质就是注册一系列终结点(Endpoint),每个终结点可以视为“路由模式”和“请求处理器”的组合,它们分别用来“选择”和“处理”请求。请求处理器通过RequestDelegate来表示,但是当我们在进行路由编程的时候,却可以使用任意类型的Delegate作为处理器器,这一切的背后是如何实现的呢?
一、指定任意类型的委托处理路由请求
二、参数绑定
三、返回值处理
一、指定任意类型的委托处理路由请求
路由终结点总是采用一个RequestDelegate委托作为请求处理器,上面介绍的这一系列终结点注册的方法提供的也都是RequestDelegate委托。实际上IEndpointConventionBuilder接口还定义了如下这些用来注册终结点的扩展方法,它们接受任意类型的委托作为处理器。
public static class EndpointRouteBuilderExtensions
{
public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler);
public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints, RoutePattern pattern, Delegate handler);
public static RouteHandlerBuilder MapMethods(this IEndpointRouteBuilder endpoints, string pattern, IEnumerable<string> httpMethods, Delegate handler);
public static RouteHandlerBuilder MapGet(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler);
public static RouteHandlerBuilder MapPost(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler);
public static RouteHandlerBuilder MapPut(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler);
public static RouteHandlerBuilder MapDelete(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler);
}
由于表示路由终结点的RouteEndpoint对象总是将RequestDelegate委托作为请求处理器,所以上述这些扩展方法提供的Delegate对象最终还得转换成RequestDelegate类型,两者之间的适配或者类型转换是由如下这个RequestDelegateFactory类型的Create方法完成的。这个方法根据提供的Delegate对象创建一个RequestDelegateResult对象,后者不仅封装了转换生成的RequestDelegate委托,终结点的元数据集合也在其中。RequestDelegateFactoryOptions是为处理器转换提供的配置选项。
public static class RequestDelegateFactory
{
public static RequestDelegateResult Create(Delegate handler,RequestDelegateFactoryOptions options = null);
} public sealed class RequestDelegateResult
{
public RequestDelegate RequestDelegate { get; }
public IReadOnlyList<object> EndpointMetadata { get; } public RequestDelegateResult(RequestDelegate requestDelegate, IReadOnlyList<object> metadata);
} public sealed class RequestDelegateFactoryOptions
{
public IServiceProvider ServiceProvider { get; set; }
public IEnumerable<string> RouteParameterNames { get; set; }
public bool ThrowOnBadRequest { get; set; }
public bool DisableInferBodyFromParameters { get; set; }
}
我并不打算详细介绍从Delegate向RequestDelegate转换的具体流程,而是通过几个简单的实例演示一下提供的各种类型的委托是如何执行的,这里主要涉及“参数绑定”和“返回值处理”两方面的处理策略。
二、参数绑定
既然可以将一个任意类型的委托终结点的处理器,意味着路由系统在执行委托的时候能够自行绑定其输入参数。这里采用的参数绑定策略与ASP.NET MVC的“模型绑定”如出一辙。当定义某个用来处理请求的方法时,我们可以在输入参数上标注一些特性显式指定绑定数据的来源,这些特性大都实现了如下这些接口。从接口命名可以看出,它们表示绑定的目标参数的原始数据分别来源于路由参数、查询字符串、请求报头、请求主体以及依赖注入容器提供的服务。
public interface IFromRouteMetadata
{
string Name { get; }
} public interface IFromQueryMetadata
{
string Name { get; }
} public interface IFromHeaderMetadata
{
string Name { get; }
} public interface IFromBodyMetadata
{
bool AllowEmpty { get; }
} public interface IFromServiceMetadata
{
}
如下这些特性实现了上面这几个接口,它们都定义在“Microsoft.AspNetCore.Mvc”命名空间下,因为它们原本是为了ASP.NET MVC下的模型绑定服务的。值得一提的是FromQueryAttribute特性不被支持,不知道是刻意为之还是把这个漏掉了。
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromRouteAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromRouteMetadata
{
public BindingSource BindingSource { get; }
public string Name { get; set; }
} [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromQueryAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromQueryMetadata
{ public BindingSource BindingSource { get; }
public string Name { get; set; }
} [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromHeaderAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromHeaderMetadata
{
public BindingSource BindingSource { get; }
public string Name { get; set; }
} [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior, IFromBodyMetadata
{
public BindingSource BindingSource { get; }
public EmptyBodyBehavior EmptyBodyBehavior { get; set; }
bool IFromBodyMetadata.AllowEmpty { get; }
} [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromServicesAttribute : Attribute, IBindingSourceMetadata, IFromServiceMetadata
{
public BindingSource BindingSource { get; }
}
如下这个演示程序调用WebApplication对象的MapPost方法注册了一个采用“/{foo}”作为模板的终结点。作为终结点处理器的委托指向静态方法Handle,我们为这个方法定义了五个参数,分别标注了上述五个特性。我们将五个参数组合成一个匿名对象作为返回值。
using Microsoft.AspNetCore.Mvc;
var app = WebApplication.Create();
app.MapPost("/{foo}", Handle);
app.Run(); static object Handle(
[FromRoute] string foo,
[FromQuery] int bar,
[FromHeader] string host,
[FromBody] Point point,
[FromServices] IHostEnvironment environment)
=> new { Foo = foo, Bar = bar, Host = host, Point = point, Environment = environment.EnvironmentName }; public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
程序启动之后,我们针对“http://localhost:5000/abc?bar=123”这个URL发送了一个POST请求,请求的主体内容为一个Point对象序列化成生成的JSON。如下所示的是请求报文和响应报文的内容,可以看出Handle方法的foo和bar参数分别绑定的是路由参数“foo”和查询字符串“bar”的值,参数host绑定的是请求的Host报头,参数point是请求主体内容反序列化的结果,参数environment则是由针对当前请求的IServiceProvider对象提供的服务。
POST http://localhost:5000/abc?bar=123 HTTP/1.1
Content-Type: application/json
Host: localhost:5000
Content-Length: 18 {"x":123, "y":456}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 06 Nov 2021 11:55:54 GMT
Server: Kestrel
Content-Length: 100 {"foo":"abc","bar":123,"host":"localhost:5000","point":{"x":123,"y":456},"environment":"Production"}
如果请求处理器方法的参数没有显式指定绑定数据的来源,路由系统也能根据参数的类型尽可能地从当前HttpContext上下文中提取相应的内容予以绑定。针对如下这几个类型,对应参数的绑定源是明确的。
- HttpContext:绑定为当前HttpContext上下文。
- HttpRequest:绑定为当前HttpContext上下文的Request属性。
- HttpResponse: 绑定为当前HttpContext上下文的Response属性。
- ClaimsPrincipal: 绑定为当前HttpContext上下文的User属性。
- CancellationToken: 绑定为当前HttpContext上下文的RequestAborted属性。
上述的绑定规则体现在如下演示程序的调试断言中。这个演示实例还体现了另一个绑定规则,那就是只要当前请求的IServiceProvider能够提供对应的服务,对应参数(“httpContextAccessor”)上标注的FromSerrvicesAttribute特性不是必要的。但是倘若缺少对应的服务注册,请求的主体内容会一般会作为默认的数据来源,所以FromSerrvicesAttribute特性最好还是显式指定为好。对于我们演示的这个例子,如果我们将前面针对AddHttpContextAccessor方法的调用移除,对应参数的绑定自然会失败,但是错误消息并不是我们希望看到的。
using System.Diagnostics;
using System.Security.Claims; var builder = WebApplication.CreateBuilder();
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
app.MapGet("/", Handle);
app.Run(); static void Handle(
HttpContext httpContext,
HttpRequest request,
HttpResponse response,
ClaimsPrincipal user,
CancellationToken cancellationToken,
IHttpContextAccessor httpContextAccessor)
{
var currentContext = httpContextAccessor.HttpContext;
Debug.Assert(ReferenceEquals(httpContext, currentContext));
Debug.Assert(ReferenceEquals(request, currentContext.Request));
Debug.Assert(ReferenceEquals(response, currentContext.Response));
Debug.Assert(ReferenceEquals(user, currentContext.User));
Debug.Assert(cancellationToken == currentContext.RequestAborted);
}
对于字符串类型的参数,路由参数和查询字符串是两个候选数据源,前者具有更高的优先级。也就是说如果路由参数和查询字符串均提供了某个参数的值,此时会优先选择路由参数提供的值。我个人倒觉得两种绑定源的优先顺序应该倒过来,查询字符串优先级似乎应该更高。对于我们自定义的类型,对应参数默认由请求主体内容反序列生成。由于请求的主体内容只有一份,所以不能出现多个参数都来源请求主体内容的情况,所以下面代码注册的终结点处理器是不合法的。
var app = WebApplication.Create();
app.MapGet("/", (Point p1, Point p2) => { });
app.Run(); public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
如果我们在某个类型中定义了一个名为TryParse的静态方法将指定的字符串表达式转换成当前类型的实例,路由系统在对该类型的参数进行绑定的时候会优先从路由参数和查询字符串中提取相应的内容,并通过调用这个方法生成绑定的参数。
var app = WebApplication.Create();
app.MapGet("/", (Point foobar) => foobar);
app.Run(); public class Point
{
public int X { get; set; }
public int Y { get; set; } public Point(int x, int y)
{
X = x;
Y = y;
}
public static bool TryParse(string expression, out Point? point)
{
var split = expression.Trim('(', ')').Split(',');
if (split.Length == 2 && int.TryParse(split[0], out var x) && int.TryParse(split[1], out var y))
{
point = new Point(x, y);
return true;
}
point = null;
return false;
}
}
上面的演示程序为自定义的Point类型定义了一个静态的TryParse方法使我们可以将一个以“(x,y)”形式定义的表达式转换成Point对象。注册的终结点处理器委托以该类型为参数,指定的参数名称为“foobar”。我们在发送的请求中以查询字符串的形式提供对应的表达式“(123,456)”,从返回的内容可以看出参数得到了成功绑定。
图1 TryParse方法针对参数绑定的影响
如果某种类型的参数具有特殊的绑定方式,我们还可以将具体的绑定实现在一个按照约定定义的BindAsync方法中。按照约定,这个BindAsync应该定义成返回类型为ValueTask<T>的静态方法,它可以拥有一个类型为HttpContext的参数,也可以额外提供一个ParameterInfo类型的参数,这两个参数分别与当前HttpContext上下文和描述参数的ParameterInfo对象绑定。前面演示实例中为Point类型定义了一个TryParse方法可以替换成如下这个 BingAsync方法。
public class Point
{
public int X { get; set; }
public int Y { get; set; } public Point(int x, int y)
{
X = x;
Y = y;
} public static ValueTask<Point?> BindAsync(HttpContext httpContext, ParameterInfo parameter)
{
Point? point = null;
var name = parameter.Name;
var value = httpContext.GetRouteData().Values.TryGetValue(name!, out var v)
? v
: httpContext.Request.Query[name!].SingleOrDefault(); if (value is string expression)
{
var split = expression.Trim('(', ')')?.Split(',');
if (split?.Length == 2 && int.TryParse(split[0], out var x) && int.TryParse(split[1], out var y))
{
point = new Point(x, y);
}
}
return new ValueTask<Point?>(point);
}
}
三、返回值处理
作为终结点处理器的委托对象不仅对输入参数没有要求,它还可以返回任意类型的对象。如果返回类型为Void、Task或者ValueTask,均表示没有返回值。如果返回类型为String、Task<String>或者ValueTask<String>,返回的字符串将直接作为响应的主体内容,响应的媒体类型会被设置为“text/plain”。对于其他类型的返回值(包括Task<T>或者ValueTask<T>),默认情况都会序列化成JSON作为响应的主体内容,响应的媒体类型会被设置为“application/json”,即使返回的是原生类型(比如Int32)也是如此。
var app = WebApplication.Create();
app.MapGet("/foo", () => "123");
app.MapGet("/bar", () => 123);
app.MapGet("/baz", () => new Point { X = 123, Y = 456});
app.Run(); public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
上面的演示程序注册了三个终结点,作为处理器的返回值分别为字符串、整数和Point对象。如果我们针对这三个终结点发送对应的GET请求,将得到如下所示的响应。
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sun, 07 Nov 2021 01:13:47 GMT
Server: Kestrel
Content-Length: 3 123
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 07 Nov 2021 01:14:11 GMT
Server: Kestrel
Content-Length: 3 123
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 07 Nov 2021 01:14:26 GMT
Server: Kestrel
Content-Length: 17 {"x":123,"y":456}
如果曾经从事过ASP.NET MVC应用的开发,应该对IActionResult接口感到很熟悉。定义在Controller类型中的Action方法一般返回会IActionResult(或者Task<IActionResult>和ValueTask<IActionResult>)对象。当Action方法执行结束后,MVC框架会直接调用返回的IActionResult对象的ExecuteResultAsync方法完整最终针对响应的处理。相同的设计同样被“移植”到这里,并为此定义了如下这个IResult接口。
public interface IResult
{
Task ExecuteAsync(HttpContext httpContext);
}
如果终结点处理器方法返回一个IResult对象或者返回一个Task<T>或ValueTask<T>(T实现了IResult接口),那么IResult对象ExecuteAsync方法将用来完成后续针对响应的处理工作。IResult接口具有一系列的原生实现类型,不过它们大都被定义成了内部类型。虽然我们不能直接调用构造函数构建它们,但是我们可以通过调用定义在Results类型中的如下这些静态方法来使用它们。
public static class Results
{
public static IResult Accepted(string uri = null, object value = null);
public static IResult AcceptedAtRoute(string routeName = null, object routeValues = null, object value = null);
public static IResult BadRequest(object error = null);
public static IResult Bytes(byte[] contents, string contentType = null, string fileDownloadName = null, bool enableRangeProcessing = false, DateTimeOffset? lastModified = default, EntityTagHeaderValue entityTag = null);
public static IResult Challenge(AuthenticationProperties properties = null, IList<string> authenticationSchemes = null);
public static IResult Conflict(object error = null);
public static IResult Content(string content, MediaTypeHeaderValue contentType);
public static IResult Content(string content, string contentType = null, Encoding contentEncoding = null);
public static IResult Created(string uri, object value);
public static IResult Created(Uri uri, object value);
public static IResult CreatedAtRoute(string routeName = null, object routeValues = null, object value = null);
public static IResult File(byte[] fileContents, string contentType = null, string fileDownloadName = null, bool enableRangeProcessing = false, DateTimeOffset? lastModified = default, EntityTagHeaderValue entityTag = null);
public static IResult File(Stream fileStream, string contentType = null, string fileDownloadName = null, DateTimeOffset? lastModified = default, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false);
public static IResult File(string path, string contentType = null, string fileDownloadName = null, DateTimeOffset? lastModified = default, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false);
public static IResult Forbid(AuthenticationProperties properties = null, IList<string> authenticationSchemes = null);
public static IResult Json(object data, JsonSerializerOptions options = null, string contentType = null, int? statusCode = default);
public static IResult LocalRedirect(string localUrl, bool permanent = false, bool preserveMethod = false);
public static IResult NoContent();
public static IResult NotFound(object value = null);
public static IResult Ok(object value = null);
public static IResult Problem(string detail = null, string instance = null, int? statusCode = default, string title = null, string type = null);
public static IResult Redirect(string url, bool permanent = false, bool preserveMethod = false);
public static IResult RedirectToRoute(string routeName = null, object routeValues = null, bool permanent = false, bool preserveMethod = false, string fragment = null);
public static IResult SignIn(ClaimsPrincipal principal, AuthenticationProperties properties = null, string authenticationScheme = null);
public static IResult SignOut(AuthenticationProperties properties = null, IList<string> authenticationSchemes = null);
public static IResult StatusCode(int statusCode);
public static IResult Stream(Stream stream, string contentType = null, string fileDownloadName = null, DateTimeOffset? lastModified = default, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false);
public static IResult Text(string content, string contentType = null, Encoding contentEncoding = null);
public static IResult Unauthorized();
public static IResult UnprocessableEntity(object error = null);
public static IResult ValidationProblem(IDictionary<string, string[]> errors, string detail = null, string instance = null, int? statusCode = default, string title = null, string type = null);
}
为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate的更多相关文章
- ASP.NET Core的路由[5]:内联路由约束的检验
当某个请求能够被成功路由的前提是它满足某个Route对象设置的路由规则,具体来说,当前请求的URL不仅需要满足路由模板体现的路径模式,请求还需要满足Route对象的所有约束.路由系统采用IRouteC ...
- ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件
虽然ASP.NET Core应用的路由是通过RouterMiddleware这个中间件来完成的,但是具体的路由解析功能都落在指定的Router对象上,不过我们依然有必要以代码实现的角度来介绍一下这个中 ...
- ASP.NET Core的路由[3]:Router的创建者——RouteBuilder
在<注册URL模式与HttpHandler的映射关系>演示的实例中,我们总是利用一个RouteBuilder对象来为RouterMiddleware中间件创建所需的Router对象,接下来 ...
- ASP.NET Core的路由[2]:路由系统的核心对象——Router
ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据以路由参数的形式解析出来供后续请求处理流 ...
- ASP.NET Core 属性路由 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 属性路由 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 属性路由 经过前面章节的学习,想必你已经对 ASP.NET Core ...
- ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系
ASP.NET Core的路由是通过一个类型为RouterMiddleware的中间件来实现的.如果我们将最终处理HTTP请求的组件称为HttpHandler,那么RouterMiddleware中间 ...
- ASP.NET Core 入门教程 3、ASP.NET Core MVC路由入门
一.前言 1.本文主要内容 ASP.NET Core MVC路由工作原理概述 ASP.NET Core MVC带路径参数的路由示例 ASP.NET Core MVC固定前/后缀的路由示例 ASP.NE ...
- 如何在ASP.NET Core中构造UrlHelper,及ASP.NET Core MVC路由讲解
参考文章: Unable to utilize UrlHelper 除了上面参考文章中介绍的方法,其实在ASP.NET Core MVC的Filter拦截器中要使用UrlHelper非常简单.如下代码 ...
- ASP.NET Core 入门笔记4,ASP.NET Core MVC路由入门
敲了一部分,懒得全部敲完,直接复制大佬的博客了,如有侵权,请通知我尽快删除修改 摘抄自https://www.cnblogs.com/ken-io/p/aspnet-core-tutorial-mvc ...
- ASP.NET Core 1.0 入门——了解一个空项目
var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...
随机推荐
- ASP.NET Core分布式项目实战(第三方ClientCredential模式调用)--学习笔记
任务10:第三方ClientCredential模式调用 创建一个控制台程序 dotnet new console --name ThirdPartyDemo 添加 Nuget 包:IdentityM ...
- .NET Core开发实战(第32课:集成事件:解决跨微服务的最终一致性)--学习笔记
32 | 集成事件:解决跨微服务的最终一致性 首先看一下集成事件的工作原理 它的目的时为了实现系统的集成,它主要是用于系统里面多个微服务之间相互传递事件 集成事件的实现方式有两种,一种是图上显示的发布 ...
- MySQL-正则表达式规范
MySQL中的正则表达式采用的是PCRE的规范,匹配时按字符进行. RLIKE 您可以使用RLIKE语句匹配正则表达式,支持的元字符如下表所示. 元字符 说明 ^ 行首. $ 行尾. . 任意字符. ...
- 【JS】强化Promise理解,从零手写属于自己的Promise.all与Promise.race
壹 ❀ 引 在一个思路搞定三道Promise并发编程题,手摸手教你实现一个Promise限制器一文中,我们在文章结尾留了一个疑问,关于第三题的实现能否解决当每次调用时间都不相等的情况(比如第二次调用要 ...
- React中refs的理解
React中refs的理解 Refs提供了一种方式,允许我们访问DOM节点或在render方法中创建的React元素. 描述 在典型的React数据流中,props是父组件与子组件交互的唯一方式,要修 ...
- java利用hdfs api进行上传下载操作
1.说明 最近项目中一部分大文件需要存储到hadoop的hdfs组件中,自己本地用3台centos7虚拟机搭建了一套集群.本地写点java代码测试一下. 代码部分改编自网络. 环境说明 一主二仆结构. ...
- 解决Oracle创建空间索引报错ORA-29855,ORA-13249,ORA-29400,ORA-01426
问题描述 公司这边用了Oracle Spatial来存储GIS数据信息,今天在某表上创建空间索引时报了下面的错: 此处举例说明: 假如有表TEST,其中有一列SHAPE存储维度信息. CREATE I ...
- kafka学习笔记03-消息生产者producer
kafka学习笔记03-消息生产者producer 发送消息整体流程示意图 消息发送的流程示意图: (From:High-level overview of Kafka producer compon ...
- Flutter——安装依赖包时,出现Waiting for another flutter command to release the startup lock
问题描述 运行 flutter packages get 时 出现 Waiting for another flutter command to release the startup lock 解决 ...
- 【LeetCode二叉树#16】二叉(搜索)树的最近公共祖先(递归后序遍历,巩固回溯机制)
二叉树的最近公共祖先 力扣题目链接(opens new window) 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先. 百度百科中最近公共祖先的定义为:"对于有根树 T 的两个结点 ...