(4)ASP.NET Core2.2 中间件

1.前言

整个HTTP Request请求跟HTTP Response返回结果之间的处理流程是一个请求管道(request pipeline)。而中间件(middleware)则是一种装配到请求管道以处理请求和响应的组件。
每个组件:
●可选择是否将请求传递到管道中的下一个组件。
●可在管道中的下一个组件前后执行工作。
中间件(middleware)处理流程如下图所示:

2.使用中间件

ASP.NET Core请求管道中每个中间件都包含一系列的请求委托(request delegates)来处理每个HTTP请求,依次调用。请求委托通过使用IApplicationBuilder类型的Run、Use和Map扩展方法在Strartup.Configure方法中配置。下面我们通过配置Run、Use和Map扩展方法示例来了解下中间件。

2.1 Run

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
    //第一个请求委托Run
    app.Run(async context =>//内嵌匿名方法
    {
        await context.Response.WriteAsync("Hello, World!");
    });
    //第二个请求委托Run
    app.Run(async context =>//内嵌匿名方法
    {
        await context.Response.WriteAsync("Hey, World!");
    });
    }
}

响应结果:

由上述代码可知,Run方法指定为一个内嵌匿名方法(称为并行中间件,in-line middleware),而内嵌匿名方法中并没有指定执行下一个请求委托,这一个过程叫管道短路,而该中间件又叫“终端中间件”(terminal middleware),因为它阻止中间件下一步处理请求。所以在Run第一个请求委托的时候就已经终止请求,并没有执行第二个请求委托直接返回Hello, World!输出文本。而根据官网解释,Run是一种约定,有些中间件组件可能会暴露他们自己的Run方法,而这些方法只能在管道末尾处运行(也就是说Run方法只在中间件执行最后一个请求委托时才使用)。

2.2 Use

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        context.Response.ContentType = "text/plain; charset=utf-8";
        await context.Response.WriteAsync("进入第一个委托 执行下一个委托之前\r\n");
        //调用管道中的下一个委托
        await next.Invoke();
        await context.Response.WriteAsync("结束第一个委托 执行下一个委托之后\r\n");
    });
    app.Run(async context =>
    {
        await context.Response.WriteAsync("进入第二个委托\r\n");
        await context.Response.WriteAsync("Hello from 2nd delegate.\r\n");
        await context.Response.WriteAsync("结束第二个委托\r\n");
    });
}

响应结果:

由上述代码可知,Use方法将多个请求委托链接在一起。而next参数表示管道中的下一个委托。如果不调用next参数调用下一个请求委托则会使管道短路。比如,一个授权(authorization)中间件只有通过身份验证之后才能调用下一个委托,否则它就会被短路,并返回“Not Authorized”的响应。所以应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。

2.3 Map和MapWhen

●Map:Map扩展基于请求路径创建管道分支。
●MapWhen:MapWhen扩展基于请求条件创建管道分支。
Map示例:

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);
        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

下面表格使用前面的代码显示来自http://localhost:5001的请求和响应。

请求响应
localhost:5001 Hello from non-Map delegate.
localhost:5001/map1  Map Test 1
localhost:5001/map2Map Test 2
localhost:5001/map3Hello from non-Map delegate.<p>

由上述代码可知,Map方法将从HttpRequest.Path中删除匹配的路径段,并针对每个请求将该路径追加到HttpRequest.PathBase。也就是说当我们在浏览器上输入map1请求地址的时候,系统会执行map1分支管道输出其请求委托信息,同理执行map2就会输出对应请求委托信息。MapWhen示例:

public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }
    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranch);
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

下面表格使用前面的代码显示来自http://localhost:5001的请求和响应。

请求响应
http://localhost:5001Hello from non-Map delegate. <p>
https://localhost:5001/?branch=masterBranch used = master

由上述代码可知,MapWhen是基于branch条件而创建管道分支的,我们在branch条件上输入master就会创建其对应管道分支。也就是说,branch条件上输入任何一个字符串条件,都会创建一个新的管理分支。而且还Map支持嵌套,例如:

public void Configure(IApplicationBuilder app)
{
    app.Map("/level1", level1App => {
        level1App.Map("/level2a", level2AApp => {
            // "/level1/level2a" processing
        });
        level1App.Map("/level2b", level2BApp => {
            // "/level1/level2b" processing
        });
    });
}

还可同时匹配多个段:

public class Startup
{
    private static void HandleMultiSeg(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map multiple segments.");
        });
    }
    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1/seg1", HandleMultiSeg);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate.");
        });
    }
}

3.顺序

向Startup.Configure方法添加中间件组件的顺序定义了在请求上调用它们的顺序,以及响应的相反顺序。此排序对于安全性、性能和功能至关重要。
以下Startup.Configure方法将为常见应用方案添加中间件组件:
●异常/错误处理(Exception/error handling)
●HTTP严格传输安全协议(HTTP Strict Transport Security Protocol)
●HTTPS重定向(HTTPS redirection)
●静态文件服务器(Static file server)
●Cookie策略实施(Cookie policy enforcement)
●身份验证(Authentication)
●会话(Session)
●MVC

请看如下代码:

public void Configure(IApplicationBuilder app)
{
    if (env.IsDevelopment())
    {
        // When the app runs in the Development environment:
        //   Use the Developer Exception Page to report app runtime errors.
        //   Use the Database Error Page to report database runtime errors.
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        // When the app doesn't run in the Development environment:
        //   Enable the Exception Handler Middleware to catch exceptions
        //     thrown in the following middlewares.
        //   Use the HTTP Strict Transport Security Protocol (HSTS)
        //     Middleware.
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
    // Return static files and end the pipeline.
    app.UseStaticFiles();
    // Authenticate before the user accesses secure resources.
    app.UseAuthentication();
}

从上述示例代码中,每个中间件扩展方法都通过Microsoft.AspNetCore.Builder命名空间在 IApplicationBuilder上公开。但是为什么我们要按照这个顺序去添加中间件组件呢?下面我们挑几个中间件来了解下。
●UseExceptionHandler(异常/错误处理)是添加到管道的第一个中间件组件。因此我们可以捕获在应用程序调用中发生的任何异常。那为什么要将异常/错误处理放在第一位呢?那是因为这样我们就不用担心因前面中间件短路而导致捕获不到整个应用程序所有异常信息。
●UseStaticFiles(静态文件)中间件在管道中提前调用,方便它可以处理请求和短路,而无需通过剩余中间组件。也就是说静态文件中间件不用经过UseAuthentication(身份验证)检查就可以直接访问,即可公开访问由静态文件中间件服务的任何文件,包括wwwroot下的文件。
●UseAuthentication(身份验证)仅在MVC选择特定的Razor页面或Controller和Action之后才会发生。
经过上面描述,大家都了解中间件顺序的重要性了吧。

4.编写中间件(重点)

虽然ASP.NET Core为我们提供了一组丰富的内置中间件组件,但在某些情况下,你可能需要根据自身业务自定义中间件,这里编写一个名叫LoggingMiddleware日志中间件记录日志,具体示例代码:

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<LoggingMiddleware> _logger;

    public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 记录请求信息
        var request = context.Request;
        _logger.LogInformation($"Request: {request.Method} {request.Path}");

        var startTime = DateTime.UtcNow;

        try
        {
            // 调用下一个中间件
            await _next(context);

            var response = context.Response;
            var elapsed = DateTime.UtcNow - startTime;

            // 记录响应信息
            _logger.LogInformation($"Response: {response.StatusCode} - {elapsed.TotalMilliseconds}ms");
        }
        catch (Exception ex)
        {
            var elapsed = DateTime.UtcNow - startTime;
            _logger.LogError(ex, $"Error: {request.Method} {request.Path} - {elapsed.TotalMilliseconds}ms");
            throw;
        }
    }
}

再定义一个日志中间件扩展类方便调用:

public static class LoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<LoggingMiddleware>();
    }
}

再通过Startup.Configure方法使用中间件:

app.UseLogging();

通过控制台看到响应结果:

通过委托构造中间件,应用程序在运行时创建这个中间件,并将它添加到管道中。这里需要注意的是,中间件的创建是单例的,每个中间件在应用程序生命周期内只有一个实例。那么问题来了,如果我们业务逻辑需要多个实例时,该如何操作呢?请继续往下看下一个示例。

6.按每次请求创建依赖注入(DI)

在中间件的创建过程中,内置的IOC容器会为我们创建一个中间件实例,并且整个应用程序生命周期中只会创建一个该中间件的实例。通常我们的程序不允许这样的注入逻辑。其实,我们可以把中间件理解成业务逻辑的入口,真正的业务逻辑是通过Application Service层实现的,我们只需要把应用服务注入到Invoke方法中即可。ASP.NET Core为我们提供了这种机制,允许我们按照请求进行依赖的注入,也就是每次请求创建一个服务。这里编写一个名叫ClientIpLoggingMiddleware客户端IP中间件记录IP信息,具体示例代码:

public class ClientIpLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ClientIpLoggingMiddleware> _logger;

    public ClientIpLoggingMiddleware(RequestDelegate next, ILogger<ClientIpLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, IIpService iIpService)
    {
        var ipAddress = iIpService.RecordIP(context);

        // 记录IP地址
        _logger.LogInformation($"Client IP: {ipAddress}, Request Path: {context.Request.Path}");

        // 调用下一个中间件
        await _next(context);
    }
}

public static class ClientIpLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseClientIpLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ClientIpLoggingMiddleware>();
    }
}

public class IpService : IIpService
{
    public string RecordIP(HttpContext context)
    {
        // 获取客户端IP地址
        var ipAddress = context.Connection.RemoteIpAddress?.ToString();

        // 如果请求经过代理,可能会在X-Forwarded-For头中包含真实的IP地址
        if (context.Request.Headers.ContainsKey("X-Forwarded-For"))
        {
            ipAddress = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
            // 如果X-Forwarded-For包含多个IP,第一个IP通常是原始客户端IP
            if (!string.IsNullOrEmpty(ipAddress))
            {
                var addresses = ipAddress.Split(',');
                if (addresses.Length > 0)
                {
                    ipAddress = addresses[0].Trim();
                }
            }
        }
        return ipAddress;
    }
}

public interface IIpService
{
    string RecordIP(HttpContext context);
}

再通过Startup.Configure方法使用中间件:

app.UseClientIpLogging();

通过控制台看到响应结果:


参考文献:
ASP.NET Core中间件https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.2
写入自定义ASP.NET Core中间件https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/write?view=aspnetcore-9.0&viewFallbackFrom=aspnetcore-2.2#per-request-dependencies

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值