[ASP.NET Core开发实战]基础篇03 中间件

什么是中间件

中间件是一种装配到应用管道,以处理请求和响应的组件。每个中间件:

  • 选择是否将请求传递到管道中的下一个中间件。
  • 可在管道中的下一个中间件前后执行。

ASP.NET Core请求管道包含一系列请求委托,依次调用。工作原理:

image

PS:类似于ASP.NET里的Handler(处理程序)和Module(模块)。

HTTP模块和处理程序的工作原理:

image

创建中间件管道

Run委托不会收到next参数。第一个Run委托始终为终端,用于终止管道。Run是一种约定。

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

Use将多个请求委托链接在一起。next参数表示管道中的下一个委托。可通过不调用next参数使用管道短路。通常可在上一个委托前后执行操作:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do work that doesn't write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}

警告:在向客户端发送Response后,不要再调用next.Invoke。Response启动后,针对HttpResponse的修改会引起异常。调用next后写入Response正文:

  • 可能导致违反协议。例如,写入的长度超过规定的Content-Length。
  • 可能损坏正文格式。例如,向CSS文件中写入HTML页脚。

HasStarted是一个有用的提示,指示是否已发送标头或已写入正文。

中间件顺序

中间件是通过请求管道逐一执行的,通过Startup.Configure方法里添加中间件的顺序来定义了请求管道的顺序。

image

如上图所示,自定义的中间件放在Authorization中间件和Endpoint之间。

以下是ASP.NET Core项目默认配置的中间件顺序:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    // app.UseCookiePolicy();

    app.UseRouting();
    // app.UseRequestLocalization();
    // app.UseCors();

    app.UseAuthentication();
    app.UseAuthorization();
    // app.UseSession();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

中间件分支

当我们需要对某些请求进行特定处理时,可以通过创建管道分支来实现。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>");
        });
    }
}

MapWhen是用于对特定谓词的结果来创建请求管道分支。相比于Map(只能对针对特定的Url)更加高级。

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

UseWhen也是用于对特定谓词的结果来创建请求管道分支。但与MapWhen不同的是,如果这个分支不发生短路或包含终端中间件,则会重新加入主管道。

public class Startup
{
    private readonly ILogger<Startup> _logger;

    public Startup(ILogger<Startup> logger)
    {
        _logger = logger;
    }

    private void HandleBranchAndRejoin(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            var branchVer = context.Request.Query["branch"];
            _logger.LogInformation("Branch used = {branchVer}", branchVer);

            // Do work that doesn't write to the Response.
            await next();
            // Do other work that doesn't write to the Response.
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranchAndRejoin);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from main pipeline.");
        });
    }
}

自定义中间件

下面以多语言版本功能为例,介绍如何编写自定义中间件。

多语言版本功能指根据所在区域,以所在区域的语言显示网站内容。例如在中国以汉字显示、在美国以英文显示、在俄罗斯以俄文显示。

下面代码是直接在Startup.Configure编写,通过参数culture手动指定语言版本,例如请求https://localhost:5001/?culture=zh-cn

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            await next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

一般来说,编写自定义中间件使用类,既符合单一职责原则,也更方便管理。

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;

            }

            // Call the next delegate/middleware in the pipeline
            await _next(context);
        }
    }
}

中间件类必须满足:

  • RequestDelegate类型参数的公共构造函数。
  • 有名字为InvokeInvokeAsync的方法,并且方法要满足:
    • 返回值是Task
    • 方法参数第一个是HttpContext类型。

在实际开发中,中间件会用到其他服务,由于中间件是在应用启动时创建的,生命周期是Singleton的,不与其他依赖注入的服务共享生命周期。如果需要使用依赖注入,可以通过InvokeInvokeAsync方法注入。

public class CustomMiddleware
{
    private readonly RequestDelegate _next;

    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    // IMyScopedService is injected into Invoke
    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}

最后,我们习惯性在Startup.Configure中,通过扩展方法来注册中间件。

using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });
    }
}

参考资料

原文地址:https://www.cnblogs.com/liang24/p/13303536.html