首页 > 编程语言 >为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate

为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate

时间:2024-03-18 11:55:56浏览次数:19  
标签:Core ASP string IResult get static NET null public

毫不夸张地说,路由是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)”,从返回的内容可以看出参数得到了成功绑定。

image

图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);
    }
}

三、返回值处理

作为终结点处理器的委托对象不仅对输入参数没有要求,它还可以返回任意类型的对象。如果返回类型为VoidTask或者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);
}

标签:Core,ASP,string,IResult,get,static,NET,null,public
From: https://www.cnblogs.com/artech/p/18075406/delgate_as_route_handler

相关文章

  • Asp.net Core关于自定义ControllerFeatureProvider的记录
    最近看公司的项目,发现公司对于自定义发现控制器搞了个方法,然后研究了一下,发现神奇现象基本原理可以查看 深入解析ASP.NETCoreMVC的模块化设计[上篇]-Artech-博客园(cnblogs.com) 大佬的博客这个是控制器的部分代码 publicclassApplicationServiceControl......
  • 开源推荐|简洁且强大的开源堡垒机OneTerm
    在运维的日常工作中,登陆服务器操作不可避免,为了更安全的管控服务器,但凡有点规模的公司都会上线堡垒机系统,堡垒机能够做到事前授权、事中监控、事后审计,同时也可以满足等保合规要求。提到堡垒机,大伙第一时间能够想到的可能是JumpServer,JumpServer经过了多年的发展已经十分的稳定和......
  • .NET集成DeveloperSharp操作Sql Server数据库
    支持.Net/.NetCore/.NetFramework,可以部署在Docker,Windows,Linux,Mac。本文提供了一种结合“原生Sql+轻量级ORM”操作各类数据库的工具。该工具几乎只使用了一个方法/函数,却实现了几乎所有的数据操作功能。它已成功应用到了人工智能、产业互联网、智慧医疗、等多个大型项......
  • ASP.NET-框架分类与详解
    一、ASP.NET框架概述ASP.NET是由微软公司推出的一种基于.NET框架的服务器端Web应用程序开发技术。它提供了丰富的工具和框架,用于开发各种规模的Web应用程序和服务。ASP.NET具有高度的灵活性和可扩展性,适用于不同规模和复杂度的项目。在ASP.NET的生态系统中,有许多不同的框......
  • Finetuning中的超参数调优
    1.背景介绍1.1机器学习与深度学习机器学习是一种让计算机系统通过经验自我改进的技术。深度学习是机器学习的一个子领域,它关注使用神经网络模型来解决复杂的问题。神经网络是一种模仿人脑工作原理的计算模型,由多个层次的节点组成,每个节点都可以处理一部分输入数据并将结......
  • NC(netcat)基本操作
    NC(netcat)基本操作前提:凭空是无法打开端口,那么我们就可以使用nc这个工具开启我们想要开放的端口。想开什么端口就可以开什么端口让别人进入。nc的启用Windows端nc使用方法Kali端nc使用方法基本使用方法一、监听/聊天工具方法:kali上使用nc去连接Windows7上的8000端......
  • Raspberries
    先看看这篇题解解释一下,首先看到\(k\)这么小,当然从\(k\)入手嘛是质数的情况,如果要操作,那么数列中肯定由\(1\)或者\(7\)组成(也可以两个都有),此时肯定一直操纵一个数是最优的,然后就有题解的结论如果\(k=4\),这个时候如果有两个及以上的偶数,肯定不用操作如果只有一个偶数,且这个偶数......
  • 【PyTorch 实战1:ResNet 分类模型】10min揭秘 ResNet如何轻松训练超深层网络以及pytorc
    ResNet简介和原理1.什么是ResNet?ResNet的目标是解决训练深层神经网络时出现的梯度消失问题。在深层网络中,梯度消失会导致难以训练。ResNet通过引入跳跃连接或快捷连接来有效地解决这个问题。由何凯明等人于2015年提出。这篇论文的正式标题是《DeepResidualLearning......
  • C#使用LINQ和EF Core
    在实际应用中,您可以使用LINQ查询EFCore来执行各种数据库操作。通过LINQ,您可以轻松地过滤、排序、分组和连接数据。要使用LINQ查询EFCore中的数据,您可以按照以下步骤进行操作:首先,确保您已经安装了EntityFrameworkCore包。然后,在您的C#项目中,创建一个继承自Db......
  • P2746 [USACO5.3] 校园网Network of Schools 题解
    题目链接:校园网NetworkofSchools这个题得翻译下题目意思才知道在干嘛,题目一开始表明了这个是一个有向图,因为边是单向的。其次关于第一个问题:基于一个事实,如果有\(x\rightarrowy\rightarrowz\),那么只需要\(x\)接受协议,它所在的\(scc\)强连通分量上的点一定都能不需要......