.net core请求管道

目录
  一、从Hello World说起

  二、管道的构成

  三、HttpApplication

  四、HttpContext

  五、Server

    六、IApplicationBuilder--用于注册中间件并创建管道

    七、Startup--利用IApplicationBuilder注册中间件

 

疑问:

  1、管道的基本组成?

  2、上下文对象是如何封装的?

  3、我们通过IApplicationBuilder的对象app.use注册的中间件是怎么样处理的?

 

一、从Hello World说起

  HTTP协议自身的特性决定了任何一个Web应用的工作方式都是监听、接收并处理HTTP请求,并在最终对请求予以响应,HTTP请求处理是管道式设计典型的应用场景。我们来创建一个最简单的Hello World程序。

 public class Program
 {
     public static void Main()
     {
         new WebHostBuilder()
             .UseKestrel()
             .UseStartup<Startup>()
             .Build()
             .Run();
     }
     public class Startup
     {
         public void Configure(IApplicationBuilder app)
         {
             app.Run(async context => await context.Response.WriteAsync("Hello World"));
         }
     }
 }

这个程序涉及到一个名为WebHost重要的对象, 它可以看成是Web应用的宿主,启动Web应用本质上就是启动作为宿主的WebHost对象。WebHostBuilder是WebHost的创建者,我们调用它的Build方法创建相应的WebHost。当我们调用WebHost的扩展方法Run启动应用的时候,用于监听、接收、处理和响应HTTP请求的管道随之被建立。总的来说,ASP.NET Core管道由WebHost在启动的时候构建,WebHostBuilder则是后者的创建者,下图揭示了三者之间的关系。

 二、管道的构成

  HTTP请求处理流程始于对请求的监听与接收,终于对请求的响应,这两项工作均由同一个对象来完成,我们称之为 “服务器(Server)” 。尽管ASP.NET Core的请求处理管道可以被自由地订制,但是该管道必须有一个Server,Server是整个管道的 “龙头” 。在上面的这个Hello World应用中,在调用WebHostBuilder的Build方法创建一个WebHost之前,我们调用了它的一个扩展方法UseKestrel,这个方法的作用就是为后续构建的管道注册一个名为KestrelServer的Server。

  随着WebHost的Start方法(当我们调用WebHost的扩展方法Run时,它的Start方法会自动被调用)的调用,定制的管道会被构建出来,管道的服务器将会绑定到一个预设的端口(比如KestrelServer默认采用5000作为监听端口)开始监听请求。HTTP请求一旦抵达,Server会并将其标准分发给管道后续的节点,我们将管道中位于服务器之后的节点称为“中间件(Middleware)”。每个中间件都具有各自独立的功能,比如我们有专门实现路由功能的中间件,有专门实施用户认证的中间件。所谓的管道定制体现在根据具体的需求选择对应的中间件组成最终处理请求的管道。下图揭示了由一个服务器和一组中间件构成的请求处理管道

 

三、HttpApplication

  ASP.NET Core请求处理管道由一个服务器和一组有序排列的中间件组合而成。我们可以在这基础上作进一步个抽象,将后者抽象成一个HttpApplication对象,那么该管道就成了一个Server和HttpApplication的综合体(如下图所示)。Server会将接收到的HTTP请求转发给HttpApplication对象,后者会针对当前请求创建一个上下文,并在此上下文中处理请求,请求处理完成并完成响应之后HttpApplication会对此上下文实施回收释放处理。

我们通过具有如下定义的IHttpApplication<TContext>类型来表示上述的这个HttpApplication,泛型参数TContext代表它针对每个请求而建立的上下文。一个HttpApplication对象在接收到Server转发的请求之后需要完成三项基本的操作,即创建上下文在上下文中处理请求以及请求处理完成之后释放上下文,这三个基本操作正好通过对应的三个方法来完成。

        public interface IHttpApplication<TContext>
        {
            TContext CreateContext(IFeatureCollection contextFeatures);
            Task ProcessRequestAsync(TContext context);
            void DisposeContext(TContext context, Exception exception);
        }

用于创建上下文的CreateContext方法具有一个类型为IFeatureCollection接口的参数。顾名思义,这个接口用于描述某个对象所具有的一组特性,我们可以将它视为一个Dictionary<Type, object>对象,字典对象的Value代表特性对象,Key则表示该对象的注册类型(可以是特性描述对象的真实类型、真实类型的基类或者实现的接口)。我们可以调用Get方法根据指定的注册类型得到设置的特性对象,特性对象的注册则通过Set方法来完成。我们自定义的FeatureCollection类型采用最简单的方式实现了这个接口。

  public interface IFeatureCollection
  {
      TFeature Get<T>();
      void Set<T>(T instance);
  }
   
  public class FeatureCollection : IFeatureCollection
  {
      private ConcurrentDictionary<Type, object> features = new ConcurrentDictionary<Type, object>();
   
      public TFeature Get<T>()
      {
          object feature;
          return features.TryGetValue(typeof(T), out feature) 
              ? (T) feature 
              : default(T);
      }
   
      public void Set<T>(T instance)
      {
          features[typeof(T)] = instance;
      }
  }

管道采用的HttpApplication是一个类型为 HostingApplication的对象。如下面的代码片段所示,这个类型实现了接口IHttpApplication<Context>,泛型参数Context是一个针对当前请求的上下文对象。一个Context对象是对一个HttpContext的封装,后者是真正描述当前HTTP请求的上下文,承载着最为核心的上下文信息。除此之外,我们还为Context定义了Scope和StartTimestamp两个属性,两者与日志记录和事件追踪有关,前者被用来将针对同一请求的多次日志记录关联到同一个上下文范围(即Logger的BeginScope方法的返回值);后者表示开始处理请求的时间戳,如果在完成请求处理的时候记录下当前的时间戳,我们就可以计算出整个请求处理所花费的时间。

 public class HostingApplication : IHttpApplication<Context>
    {
        public RequestDelegate Application { get; }

        public HostingApplication(RequestDelegate application)
        {
            this.Application = application;
        }

        public Context CreateContext(IFeatureCollection contextFeatures)
        {
            HttpContext httpContext = new DefaultHttpContext(contextFeatures);
            return new Context
            {
                HttpContext = httpContext,
                StartTimestamp = Stopwatch.GetTimestamp()
            };
        }

        public void DisposeContext(Context context, Exception exception)=> context.Scope?.Dispose();
        public Task ProcessRequestAsync(Context context)=> this.Application(context.HttpContext);
    }

    public class Context
    {
        public HttpContext HttpContext { get; set; }
        public IDisposable Scope { get; set; }
        public long StartTimestamp { get; set; }
    }

四、HttpContext

  用来描述当前HTTP请求的上下文的HttpContext对于ASP .NET Core请求处理管道来说是一个非常重要的对象,我们不仅仅可以利用它获取当前请求的所有细节,还可以直接利用它完成对请求的响应。HttpContext是一个抽象类,很多用于描述当前HTTP请求的上下文信息的属性被定义在这个类型中。在这个这个模拟管道模型中,我们仅仅保留了如下两个核心的属性,即表示请求和响应的Requst和Response属性。

  public abstract class HttpContext
  {
      public abstract HttpRequest     Request { get; }
      public abstract HttpResponse    Response { get; }
  }

表示请求和响应的HttpRequest和HttpResponse同样是抽象类。简单起见,我们仅仅保留少数几个与演示实例相关的属性成员。如下面的代码片段所示,我们仅仅为HttpRequest保留了表示当前请求地址的Url属性和表示基地址的PathBase属性。对于HttpResponse来说,我们保留了三个分别表示输出流(OutputStream)、媒体类型(ContentType)和响应状态码(StatusCode)的属性。

 public abstract class HttpRequest
 {
     public abstract Uri    Url { get; }
     public abstract string PathBase { get; }
 }
  
 public abstract class HttpResponse
 {
     public abstract Stream     OutputStream { get; }
     public abstract string     ContentType { get; set; }
     public abstract int        StatusCode { get; set; }
 }

ASP.NET Core默认使用的HttpContext是一个类型为DefaultHttpContext对象,在介绍DefaultContext的实现原理之前,我们必须了解这样一个事实:对应这个管道来说,请求的接收者和最终响应者都是服务器,服务器接收到请求之后会创建自己的上下文来描述当前请求,针对请求的响应也通过这个原始上下文来完成。以我应用中注册的HttpListenerServer为例,由于它内部使用的是一个类型为HttpListener的监听器,所以它总是会创建一个HttpListenerContext对象来描述接收到的请求,针对请求的响应也是利用这个HttpListenerContext对象来完成的。

但是对于建立在管道上的应用来说,它们是不需要关注管道究竟采用了何种类型的服务器,更不会关注由这个服务器创建的这个原始上下文。实际上我们的应用不仅统一使用这个DefaultHttpContext对象来获取请求信息,同时还利用它来完成对请求的响应。很显然,应用这使用的这个DefaultHttpContext对象必然与服务器创建的原始上下文存在某个关联,这种关联是通过上面我们提到过的这个FeatureCollection对象来实现的。

如上图所示,不同类型的服务器在接收到请求的时候会创建一个原始的上下文,接下来它会将针对原始上下文的操作封装成一系列标准的特性对象(特性类型实现统一的接口)。这些特性对象最终服务器被组装成一个FeatureCollection对象,应用程序中使用的DefaultHttpContext就是根据它创建出来的。当我们调用DefaultHttpContext相应的属性和方法时,在它的内部实际上借助封装的特性对象去操作原始的上下文。

一旦了解DefaultHttpContext是如何操作原始HTTP上下文之后,对于DefaultHttpContext的定义就很好理解了。如下面的代码片断所示,DefaultHttpContext具有一个IFeatureCollection类型的属性HttpContextFeatures,它表示的正是由服务器创建的用于封装原始HTTP上下文相关特性的FeatureCollection对象。通过构造函数的定义我们知道对于一个DefaultHttpContext对象来说,表示请求和响应的分别是一个DefaultHttpRequest和DefaultHttpResponse对象。

public class DefaultHttpContext : HttpContext
{ 
    public IFeatureCollection HttpContextFeatures { get;}
 
    public DefaultHttpContext(IFeatureCollection httpContextFeatures)
    {
        this.HttpContextFeatures = httpContextFeatures;
        this.Request      = new DefaultHttpRequest(this);
        this.Response     = new DefaultHttpResponse(this);
    }
    public override HttpRequest      Request { get; }
    public override HttpResponse     Response { get; }
}

由不同类型的服务器创建的特性对象之所以能够统一被DefaultHttpContext所用,原因在于它们的类型都实现统一的接口,在模拟的管道模型中,我们定义了如下两个针对请求和响应的特性接口IHttpRequestFeature和IHttpResponseFeature,它们与HttpRequest和HttpResponse具有类似的成员定义。

public interface IHttpRequestFeature
{
    Uri    Url { get; }
    string PathBase { get; }
}
 
public interface IHttpResponseFeature
{
    Stream     OutputStream { get; }
    string     ContentType { get; set; }
    int        StatusCode { get; set; }
}

实际上DefaultHttpContext对象中表示请求和响应的DefaultHttpRequest和DefaultHttpResponse对象就是分别根据从提供的FeatureCollection中获取的HttpRequestFeature和HttpResponseFeature对象创建的,具体的实现体现在如下所示的代码片断中。

public class DefaultHttpRequest : HttpRequest
{
    public IHttpRequestFeature RequestFeature { get; }
    public DefaultHttpRequest(DefaultHttpContext context)
    {
        this.RequestFeature = context.HttpContextFeatures.Get<IHttpRequestFeature>();
    }
    public override Uri Url
    {
        get { return this.RequestFeature.Url; }
    }
 
    public override string PathBase
    {
        get { return this.RequestFeature.PathBase; }
    }
}
public class DefaultHttpResponse : HttpResponse
{
    public IHttpResponseFeature ResponseFeature { get; }
 
    public override Stream OutputStream
    {
        get { return this.ResponseFeature.OutputStream; }
    }
 
    public override string ContentType
    {
        get { return this.ResponseFeature.ContentType; }
        set { this.ResponseFeature.ContentType = value; }
    }
 
    public override int StatusCode
    {
        get { return this.ResponseFeature.StatusCode; }
        set { this.ResponseFeature.StatusCode = value; }
    }
 
    public DefaultHttpResponse(DefaultHttpContext context)
    {
        this.ResponseFeature = context.HttpContextFeatures.Get<IHttpResponseFeature>();
    }
}

在了解了DefaultHttpContext的实现原理之后,我们在回头看看上面作为默认HttpApplication类型的HostingApplication的定义。由于对请求的处理总是在一个由HttpContext对象表示的上下文中进行,所以针对请求的处理最终可以通过具有如下定义的RequestDelegate委托对象来完成。一个HttpApplication对象可以视为对一组中间件的封装,它对请求的处理工作最终交给这些中间件来完成,所有中间件对请求的处理最终可以转换成一个RequestDelegate对象,HostingApplication的Application属性返回的就是这么一个RequestDelegate对象。

public class HostingApplication : IHttpApplication<Context>
{
    public RequestDelegate Application { get; }
 
    public HostingApplication(RequestDelegate application)
    {
        this.Application = application;
    }
 
    public Context CreateContext(IFeatureCollection contextFeatures)
    {
        HttpContext httpContext = new DefaultHttpContext(contextFeatures);
        return new Context
        {
            HttpContext     = httpContext,
            StartTimestamp  = Stopwatch.GetTimestamp()
        };
    }
 
    public void DisposeContext(Context context, Exception exception) => context.Scope?.Dispose();
    public Task ProcessRequestAsync(Context context) => this.Application(context.HttpContext);
}
 
public delegate Task RequestDelegate(HttpContext context);

当我们创建一个HostingApplication对象的时候,需要将所有注册的中间件转换成一个RequestDelegate类型的委托对象,并将其作为构造函数的参数,ProcessRequestAsync方法会直接利用这个委托对象来处理请求。当CreateContext方法被执行的时候,它会直接利用封装原始HTTP上下文的FeatureCollection对象创建一个DefaultHttpContext对象,进而一个Context对象。在简化的DisposeContext方法中,我们只是调用了Context对象的Scope属性的Dispose方法(如果Scope存在),实际上我们在创建Context的时候并没有Scope属性进行初始化。

我们依然通过一个UML对表示HTTP上下文相关的接口/类型及其相互关系进行总结。如下图所示,针对当前请求的HTTP上下文通过抽象类HttpContext表示,请求和响应是HttpContext表述的两个最为核心的上下文请求,它们分别通过抽象类HttpRequest和HttpResponse表示。ASP.NET Core 默认采用的HttpContext类型为DefaultHttpContext,它描述的请求和响应分别是一个DefaultHttpRequst和DefaultHttpResponse对象。一个DefaultHttpContext对象由描述原始HTTP上下文的特性集合来创建,其中描述请求与相应的特性分别通过接口IHttpRequestFeature和IHttpResponseFeature表示,DefaultHttpRequst和DefaultHttpResponse正是分别根据它们创建的。

五、服务器

  管道中的服务器通过IServer接口表示,在模拟管道对应的应用编程接口中,我们只保留了两个核心成员,其中Features属性返回描述服务器的特性,而Start方法则负责启动服务器。Start方法被执行的时候,服务会马上开始实施监听工作。HTTP请求一旦抵达,该方法会利用作为参数的HttpApplication对象创建一个上下文,并在此上下文中完成对请求的所有处理操作。当完成了对请求的处理任务之后,HttpApplication对象会自行负责回收释放由它创建的上下文。

public interface IServer
{
    IFeatureCollection Features { get; }
    void Start<TContext>(IHttpApplication<TContext> application);    
}

在我们演示的发布图片应用中使用的服务器是一个类型为HttpListenerServer的服务器。顾名思义,这个简单的服务器直接利用HttpListener来完成对请求的监听、接收和响应工作。这个HttpListener对象通过Listener这个只读属性表示,我们在构造函数中创建它。对于这个HttpListener,我们并没有直接为他指定监听地址,监听地址的获取是通过一个由IServerAddressesFeature接口表示的特性来提供的。如下面的代码片段所示,这个特性接口通过一个字符串集合类型的Addresses属性表示监听地址列表,ServerAddressesFeature是这个特性接口的默认实现类型。在构造函数中,我们在初始化Features属性之后,会添加一个ServerAddressesFeature对象到这个特性集合中。

public class HttpListenerServer : IServer
{
    public HttpListener         Listener { get; }
    public IFeatureCollection     Features { get; }
 
    public HttpListenerServer()
    {
        this.Listener = new HttpListener();
        this.Features = new FeatureCollection()
            .Set<IServerAddressesFeature>(new ServerAddressesFeature());
    }
    ...
}
 
public interface IServerAddressesFeature
{
    ICollection<string> Addresses { get; }
}
 
public class ServerAddressesFeature : IServerAddressesFeature
{
    public ICollection<string> Addresses { get; } = new Collection<string>();
}

在Start方法中,我们从特性集合中提取出这个ServerAddressesFeature对象,并将设置的监听地址集合注册到HttpListener对象上,然后调用其Start方法开始监听来自网络的HTTP请求。HTTP请求一旦抵达,我们会调用HttpListener的GetContext方法得到表示原始HTTP上下文的HttpListenerContext对象,并根据它创建一个类型为HttpListenerContextFeature的特性对象,该对象分别采用类型IHttpRequestFeature和IHttpResponseFeature注册到创建的FeatureCollection对象上。作为参数的HttpApplication对象将它作为参数调用CreateContext方法创建出类型为TContext的上下文对象,我们最终将它作为参数调用HttpApplication对象的ProcessRequestAsync方法让注册的中间件来处理当前请求。当所有的请求处理工作结束之后,我们会调用HttpApplication对象的DisposeContext方法回收释放这个上下文。

public class HttpListenerServer : IServer
{
    ...
    public void Start<TContext>(IHttpApplication<TContext> application)
    {
        IServerAddressesFeature addressFeatures = this.Features.Get<IServerAddressesFeature>();
        foreach (string address in addressFeatures.Addresses)
        {
            this.Listener.Prefixes.Add(address.TrimEnd('/') + "/");
        }
 
        this.Listener.Start();
        while (true)
        {
            HttpListenerContext httpListenerContext = this.Listener.GetContext();
 
            HttpListenerContextFeature feature = new HttpListenerContextFeature(httpListenerContext, this.Listener);
            IFeatureCollection contextFeatures = new FeatureCollection()
                .Set<IHttpRequestFeature>(feature)
                .Set<IHttpResponseFeature>(feature);
            TContext context = application.CreateContext(contextFeatures);
 
            application.ProcessRequestAsync(context)
                .ContinueWith(_ => httpListenerContext.Response.Close())
                .ContinueWith(_ => application.DisposeContext(context, _.Exception));
        }
    }
}

由于HttpListenerServer采用一个HttpListener对象作为监听器,由它接收的请求将被封装成一个类型为HttpListenerContext的上下文对象。我们通过一个HttpListenerContextFeature类型来封装这个HttpListenerContext对象。如下面的代码片段所示,HttpListenerContextFeature实现了IHttpRequestFeature和IHttpResponseFeature接口,HttpApplication所代表的中间件不仅仅利用这个特性获取所有与请求相关的信息,而且针对请求的任何响应也都是利用这个特性来实现的。

public class HttpListenerContextFeature : IHttpRequestFeature, IHttpResponseFeature
{
    private readonly HttpListenerContext context;    
 
    public string ContentType
    {
        get { return context.Response.ContentType; }
        set { context.Response.ContentType = value; }
    }
 
    public Stream OutputStream { get; }
 
    public int StatusCode
    {
        get { return context.Response.StatusCode; }
        set { context.Response.StatusCode = value; }
    }
 
    public Uri Url { get; }
    public string PathBase { get; }
 
    public HttpListenerContextFeature(HttpListenerContext context, HttpListener listener)
    {
        this.context = context;
        this.Url = context.Request.Url;
        this.OutputStream = context.Response.OutputStream;
        this.PathBase = (from it in listener.Prefixes
          let pathBase = new Uri(it).LocalPath.TrimEnd('/')
          where context.Request.Url.LocalPath.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)
          select pathBase).First();
    }
}

下图所示的UML体现了与服务器相关的接口/类型之间的关系。通过接口IServer表示的服务器表示管道中完成请求监听、接收与相应的组件,我们自定义的HttpListenerServer利用一个HttpListener实现了这三项基本操作。当HttpListenerServer接收到抵达的HTTP请求之后,它会将表示原始HTTP上下文的特性封装成一个HttpListenerContextFeature对象,HttpListenerContextFeature实现了分别用于描述请求和响应特性的接口IHttpRequestFeature和IHttpResponseFeature,HostingApplication可以利用这个HttpListenerContextFeature对象来创建DefaultHttpContext对象。

 六、IApplicationBuilder--用于注册中间件并创建管道

  我们所说的ApplicationBuilder是对所有实现了IApplicationBuilder接口的所有类型及其对象的统称。用于创建WebHost的WebHostBuilder具有一个用于管道定值的Configure方法,它利用作为参数的ApplicationBuilder对象进行中间件的注册。由于ApplicationBuilder与组成管道的中间件具有直接的关系,所以我们得先来说说中间件在管道中究竟体现为一个怎样的对象。

中间件在请求处理流程中体现为一个类型为Func<RequestDelegate,RequestDelegate>的委托对象,在大部分应用中,我们会针对具体的请求处理需求注册多个不同的中间件,这些中间件按照注册时间的先后顺序进行排列进而构成管道。对于某个中间件来说,在它完成了自身的请求处理任务之后,需要将请求传递给下一个中间件作后续的处理。Func<RequestDelegate,RequestDelegate>中作为输入参数的RequestDelegate对象代表一个委托链,体现了后续中间件对请求的处理。一般来说,当某个中间件将自身实现的请求处理任务添加到这个委托链中,新的委托链将作为这个Func<RequestDelegate,RequestDelegate>对象的返回值。

以下图所示的管道为例,如果用一个Func<RequestDelegate,RequestDelegate>来表示中间件B,那么作为输入参数的RequestDelegate对象代表的是C对请求的处理操作,而返回值则代表B和C先后对请求处的处理操作。如果一个Func<RequestDelegate,RequestDelegate>代表第一个从服务器接收请求的中间件(比如A),那么执行该委托对象返回的RequestDelegate实际上体现了整个管道对请求的处理。

在对中间件有了充分的了解之后,我们来看看用于注册中间件的IApplicationBuilder接口的定义。如下所示的是经过裁剪后的IApplicationBuilder接口的定义,我们只保留了两个核心的方法,其中Use方法实现了针对中间件的注册,另一个Build方法则将所有注册的中间件转换成一个RequestDelegate对象。

public interface IApplicationBuilder
{
    RequestDelegate Build();
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}

从编程便利性考虑,很多预定义的中间件类型都具有对应的扩展方法进行注册,比如我们调用扩展方法UseStaticFiles来注册处理静态文件请求的中间件。对于我们演示的发布图片的应用来说,它也是通过调用一个具有如下定义的扩展方法UseImages来注册处理图片请求的中间件。这个UseImages方法的rootDirectory参数代表存放图片的目录,在这个方法中我们创建了一个Func<RequestDelegate, RequestDelegate>对象,这个委托对象会根据当前请求的URL和PathBase解析出目标图片的真实路径,并最终将文件内容写入到响应的输出流中。

public static class Extensions
{
    private static Dictionary<string, string> mediaTypeMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
    static Extensions()
    {
        mediaTypeMappings.Add(".jpg", "image/jpeg");
        mediaTypeMappings.Add(".gif", "image/gif");
        mediaTypeMappings.Add(".png", "image/png");
        mediaTypeMappings.Add(".bmp", "image/bmp");
    }
 
    public static IApplicationBuilder UseImages(this IApplicationBuilder app, string rootDirectory)
    {
        Func<RequestDelegate, RequestDelegate> middleware = next =>
        {
            return async context =>
            {
                string filePath = context.Request.Url.LocalPath.Substring(context.Request.PathBase.Length + 1);
                filePath = Path.Combine(rootDirectory, filePath).Replace('/', Path.DirectorySeparatorChar);
                filePath = File.Exists(filePath)
                    ? filePath
                    : Directory.GetFiles(Path.GetDirectoryName(filePath)).FirstOrDefault(it => string.Compare(Path.GetFileNameWithoutExtension(it), Path.GetFileName(filePath), true) == 0);
 
                if (!string.IsNullOrEmpty(filePath))
                {
                    string extension = Path.GetExtension(filePath);
                    string mediaType;
                    if (mediaTypeMappings.TryGetValue(extension, out mediaType))
                    {
                        await context.Response.WriteFileAsync(filePath, "image/jpg");
                    }
                }
                await next(context);
            };
        };
 
        return app.Use(middleware);
    }
 
    public static async Task WriteFileAsync(this HttpResponse response, string fileName, string contentType)
    {
        if (File.Exists(fileName))
        {
            byte[] content = File.ReadAllBytes(fileName);
            response.ContentType = contentType;
            await response.OutputStream.WriteAsync(content, 0, content.Length);
        }
        response.StatusCode = 404;
    }
}

针对图片文件内容的响应实现在另一个针对HttpResponse的扩展方法WriteFileAsync中。除了将图片文件的内容写入响应的输出流中,我们还需要针对图片的类型为响应设置对应的媒体类型(对应着HttpResponse的ContentType属性)。严格来说,媒体类型应该由读取的文件内容来确定,简单起见,我们指定的媒体类型是通过图片文件的扩展名推导出来的。

我们定义了一个ApplicationBuilder类型来作为IApplicationBuilder的默认实现者。如下面的代码片段所示,我们采用一个List<Func<RequestDelegate, RequestDelegate>>对象来存放所有注册的中间件,在Build方法中,我们调用它的Aggregate方法将它转换成一个RequestDelegate对象。

public class ApplicationBuilder : IApplicationBuilder
{
    private IList<Func<RequestDelegate, RequestDelegate>> middlewares = new List<Func<RequestDelegate, RequestDelegate>>();  
 
    public RequestDelegate Build()
    {
        RequestDelegate seed = context => Task.Run(() => {});
        return middlewares.Reverse().Aggregate(seed, (next, current) => current(next));
    }    
 
    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        middlewares.Add(middleware);
        return this;
    }
}

七、Startup--利用IApplicationBuilder注册中间件

  一个服务器和一组中间件组成了ASP .NET Core的HTTP请求处理管道,中间件的注册通过调用ApplicationBuilder的Use方法来完成。中间件的注册以及管道的构建是应用启动时所作的一项核心工作,ASP.NET Core为此专门定义了一个IStarup接口来从事启动时的初始化工作,我们将实现这个接口的类型以及对应对象统称为Startup。对于模拟管道的这个同名接口来说,我们对它进行了简化,只保留了如下一个唯一的Configure方法。由于这个Configure方法的主要目的在于为构建的管道注册相应的中间件,所以该方法具有的唯一参数是一个ApplicationBuilder对象。

public interface IStartup
{
    void Configure(IApplicationBuilder app);
}

定义在IStarup接口中的Configure方法以用于注册中间件的ApplicationBuilder对象作为输入,所以这个方法其实体现为一个Action<IApplicationBuilder>对象,所以我们在模拟的管道中定义了如下一个DelegateStartup类型来作为这个IStarup接口的默认实现。

public class DelegateStartup : IStartup
{
    private Action<IApplicationBuilder> _configure;
 
    public DelegateStartup(Action<IApplicationBuilder> configure)
    {
        _configure = configure;
    }
 
    public void Configure(IApplicationBuilder app)
    {
        configure(app);
    }
}

 

源代码:链接

提取码:fw06

参考文档:Artech系列博客

 

posted @ 2019-06-07 14:12  Uncle_Drew  阅读(564)  评论(0)    收藏  举报