一、概述

本篇介绍如何使用中间件(Middleware)。

 

二、初步演练

先写几个中间件



public class DemoAMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

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

        public async Task Invoke(HttpContext context)
        {
            _logger.LogInformation("(1) DemoAMiddleware.Invoke front");  
            await _next(context);
            _logger.LogInformation("[1] DemoAMiddleware:Invoke back");
        }       
    }

  public class DemoBMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

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

        public async Task Invoke(HttpContext context)
        {

            _logger.LogInformation("(2) DemoBMiddleware.Invoke part1");  
            await _next(context);
            _logger.LogInformation("[2] DemoBMiddleware:Invoke part2");
        }      
    }

    public class RequestRecordMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

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

        public async Task Invoke(HttpContext context)
        {
            _logger.LogInformation("(3) RequestRecordMiddleware.Invoke");

            String URL = context.Request.Path.ToString();
            _logger.LogInformation($"URL : {URL}");            

            await _next(context);

            _logger.LogInformation("[3] RequestRecordMiddleware:Invoke next");
            _logger.LogInformation($"StatusCode : {context.Response.StatusCode}");
        }       
    }



以上中间件前两个没有做什么正经工作,就打印一些日志信息,第三个干了一点工作,它打印了用户输入的url,同时打印了返回给客户端的状态码。

要使中间件工作,需要启用中间件。



public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {            
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            app.UseUnifyException();

            app.UseMiddleware<DemoAMiddleware>();
            app.UseMiddleware<DemoBMiddleware>();           
            app.UseMiddleware<RequestRecordMiddleware>();            

            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }



通过扩展方法,我们对中间件的启用代码进行改造:



public static class RequestRecordMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestRecord(this IApplicationBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException("builder is null");
            }

            return builder.UseMiddleware<RequestRecordMiddleware>();
        }
    }



此时启用代码由:app.UseMiddleware<RequestRecordMiddleware>(); 

可以修改为:   app.UseRequestRecord();

实现效果没有变化。可见下面代码都是中间件的使用。



app.UseStaticFiles();
app.UseMvcWithDefaultRoute();



  

我的理解,中间件类似车间流水线上的工人,操作的零件就是HttpContext,每个人负责两道工序,我把它们定义为“前道工序”和“后道工序”,通过代码 _next(context); 把两道工序隔离开,处理的顺序需要特别注意,按照中间件注册的顺序依次处理“前道工序”,处理完成后,再按相反的顺序处理“后道工序”,如果某个中间件没有调用_next(context),那么就不会调用后续的中间件,所以中间件启用的顺序是需要特别考虑的。

以上代码中三个中间件输出到控制台的信息顺序如下:


个人认为,“前道工序”应重点处理Request,“后道工序”应重点处理Response。

 

三、做一个类似MVC的中间件

我们做一个中间件,让其返回一些内容给客户端:



public class MyMvcMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

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

        public async Task Invoke(HttpContext context)
        {  var str = "hello,world!";
            await  context.Response.WriteAsync(str);          
        }       
    }



这个中间件只是返回固定的字符串,我们还可以调用某个Controller的提供的方法。



public class MyMvcMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

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

        public async Task Invoke(HttpContext context)
        {
            var obj = context.RequestServices.GetRequiredService<ArticleController>().GetArticleList();
            var str = JsonConvert.SerializeObject(obj);
            await context.Response.WriteAsync(str);
        }
    }



ArticleController的定义如下:



public class ArticleController : Controller
    {
        private readonly SalesContext _context;
        private readonly ILogger _logger;
        private readonly IMemoryCache _cache;

        public ArticleController(SalesContext context, ILogger<ArticleController> logger, IMemoryCache memoryCache)
        {
            _context = context;
            _logger = logger;
            _cache = memoryCache;
        }

        [HttpGet]
        public ResultObject GetArticleList()
        {
            _logger.LogInformation("==GetArticleList==");

            List<Article> articles = _context.Articles
                .AsNoTracking()
                .ToList();

            return new ResultObject
            {               
                result = articles
            };
        }  
    }



要在中间件中通过依赖使用该Controller,需要将其注册到DI容器:



public void ConfigureServices(IServiceCollection services)
{ 
      services.AddScoped<ArticleController>();
}



以上中间件实现了一个文章信息查询的功能,如果在此中间件内先判断路径,再根据不同的路径调用不同的Contorller提供的服务,就可以形成一个简单的MVC中间件了。

 

四、中间件的用途

 中间件的使用体现了AOP(面向切片)的编程思想,在不修改现有代码的情况下,通过增加一些中间件实现一些特定逻辑,可以做的事情很多,比如:

URL重写

缓存处理

异常处理

用户认证

 

五、中间件的注册顺序

前文提到中间件的注册顺序是比较重要的,建议的顺序如下:

1. 异常/错误处理
2. 静态文件服务器
3. 身份验证
4. MVC