Fork me on GitHub
RainingNight
雨夜朦胧

ASP.NET Core 运行原理解剖[4]:进入HttpContext的世界

HttpContext是ASP.NET中的核心对象,每一个请求都会创建一个对应的HttpContext对象,我们的应用程序便是通过HttpContext对象来获取请求信息,最终生成响应,写回到HttpContext中,完成一次请求处理。在前面几章中也都有提到HttpContext,本章就来一起探索一下HttpContext的世界,揭开它的神秘面纱。

目录

本系列文章从源码分析的角度来探索 ASP.NET Core 的运行原理,分为以下几个章节:

ASP.NET Core 运行原理解剖[1]:Hosting

ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍

ASP.NET Core 运行原理解剖[3]:Middleware-请求管道的构成

ASP.NET Core 运行原理解剖[4]:进入HttpContext的世界(Current)

  1. IHttpContextFactory
  2. IFeatureCollection
  3. HttpContext
  4. IHttpContextAccessor

ASP.NET Core 运行原理解剖[5]:Authentication

IHttpContextFactory

第一章中,我们介绍到,WebHost 在启动 IServer 时,会传入一个 IHttpApplication<TContext> 类型的对象,Server 负责对请求的监听,在接收到请求时,会调用该对象的 ProcessRequestAsync 方法将请求转交给我们的应用程序。IHttpApplication<TContext> 的默认实现为 HostingApplication ,有如下定义:

public class HostingApplication : IHttpApplication<HostingApplication.Context>
{
    private readonly RequestDelegate _application;
    private readonly IHttpContextFactory _httpContextFactory;

    public Context CreateContext(IFeatureCollection contextFeatures)
    {
        var context = new Context();
        var httpContext = _httpContextFactory.Create(contextFeatures);

        _diagnostics.BeginRequest(httpContext, ref context);

        context.HttpContext = httpContext;
        return context;
    }

    public Task ProcessRequestAsync(Context context)
    {
        return _application(context.HttpContext);
    }

    public void DisposeContext(Context context, Exception exception)
    {
        var httpContext = context.HttpContext;
        _diagnostics.RequestEnd(httpContext, exception, context);
        _httpContextFactory.Dispose(httpContext);
        _diagnostics.ContextDisposed(context);
    }
}

首先使用 IHttpContextFactory 来创建 HttpContext 实例,然后在 ProcessRequestAsync 方法中调用上一章介绍的 RequestDelegate,由此进入到我们的应用程序当中。

IHttpContextFactory 负责对 HttpContext 的创建和释放,分别对应着CreateDispose方法,它的默认实现类为HttpContextFactory,定义如下:

public class HttpContextFactory : IHttpContextFactory
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly FormOptions _formOptions;

    public HttpContext Create(IFeatureCollection featureCollection)
    {
        var httpContext = new DefaultHttpContext(featureCollection);
        if (_httpContextAccessor != null)
        {
            _httpContextAccessor.HttpContext = httpContext;
        }

        var formFeature = new FormFeature(httpContext.Request, _formOptions);
        featureCollection.Set<IFormFeature>(formFeature);

        return httpContext;
    }

    public void Dispose(HttpContext httpContext)
    {
        if (_httpContextAccessor != null)
        {
            _httpContextAccessor.HttpContext = null;
        }
    }
}

如上,HttpContextFactory 只是简单的使用 new DefaultHttpContext(featureCollection) 来创建 HttpContext 的实例,而这里涉及到一个 IFeatureCollection 对象,它是由 Server 根据原始请求创建而来的,下面就先介绍一下该对象。

IFeatureCollection

不过,在介绍 IFeatureCollection 之前,我们先需先回顾一下OWIN:

OWIN是 “Open Web Server Interface for .NET” 的首字母缩写,它定义了一套Web Server和Web Application之间的标准接口,主要用于解除 ASP.NET 与 IIS 的紧密耦合。为此,OWIN 定义了四个核心组件:Host, Server, Middleware, Application,并为Server和Middleware的之间的交互提供了一个 Func<IDictionary<string,object>,Task> 类型的标准接口。

每一个OWIN中间件,都会接收到一个 IDictionary<string,object> 类型的变量,用来表示当前请求的相关信息,也称为环境字典。每一个支持OWIN标准的 Web Server 都会根据请求的原始上下文信息,封装成这个环境字典,然后在OWIN中间件之间传递,进而完成整个请求的处理。环境字典定义了一系列预先约定好的Key,比如:用 "owin.RequestBody" 来表示请求体,"owin.RequestHeaders" 来表示请求头,"owin.RequestMethod" 来表示请求方法等。

OWIN是随着ASP.NET MVC5进行到我们的视线中,在当时,ASP.NET WebAPI 2.0 也基于OWIN实现了自寄宿模式。再后来,提出了 ASP.NET 5 与 MVC6,完全是基于OWIN的模式来开发的,再到今天的 ASP.NET Core,OWIN的概念已被模糊化了,但是还是随处可以见到OWIN的影子,并且也提供了对 OWIN 的扩展支持。

在 ASP.NET Core 中,提出了 IFeatureCollection 的概念,它本质上也是一个 IDictionary<string,object> 键值对,但是它具有面向对象的特点,相对于 IDictionary<string,object> 更加清晰,容易理解,并且Server构建成这样一个对象也很容易,它有如下定义:

public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>>
{
    bool IsReadOnly { get; }

    int Revision { get; }

    object this[Type key] { get; set; }

    TFeature Get<TFeature>();

    void Set<TFeature>(TFeature instance);
}

它的定义非常简单,由一系列以键值对来表示的标准特性对象(TFeature)组成,可以通过一个索引以及 GetSet 方法来获取或设置这些特性对象。

下面,我们看一下在 ASP.NET Core 中的对它的一个模拟实现:

public class FeatureCollection : IFeatureCollection
{
    private IDictionary<Type, object> _features;
    private readonly IFeatureCollection _defaults;
    private volatile int _containerRevision;

    public virtual int Revision
    {
        get { return _containerRevision + (_defaults?.Revision ?? 0); }
    }

    public object this[Type key]
    {
        get
        {
            object result;
            return _features != null && _features.TryGetValue(key, out result) ? result : _defaults?[key];
        }
        set
        {
            if (value == null)
            {
                if (_features != null && _features.Remove(key))
                {
                    _containerRevision++;
                }
                return;
            }

            if (_features == null)
            {
                _features = new Dictionary<Type, object>();
            }
            _features[key] = value;
            _containerRevision++;
        }
    }

    public TFeature Get<TFeature>()
    {
        return (TFeature)this[typeof(TFeature)];
    }

    public void Set<TFeature>(TFeature instance)
    {
        this[typeof(TFeature)] = instance;
    }    
}

如上,它的内部属性 _features 便是OWIN中的标准环境字典,并且提供了更加方便的泛型 Get, Set 方法,以及一个索引器来访问该环境字典。不过,如果只是这样,那使用起来依然不够方便,更为重要的是 ASP.NET Core 还提供了一系列的特性对象,并以这些特性对象的类型做为环境字典中的Key。

通过上面代码,还可以发现,每次对该环境字典的修改,都会使 Revision 属性递增1。

这里为什么说FeatureCollection是一个模拟的实现呢?具我观察,FeatureCollection对象只在ASP.NET Core的测试代码中用到,而每个Server都有它自己的方式来构建IFeatureCollection,并不会使用FeatureCollection,关于Server中是如何创建IFeatureCollection实例的,可以参考KestrelHttpServer中的实现,这里就不再深究。

那特性对象又是什么呢?我们先看一下请求特性的定义:

public interface IHttpRequestFeature
{
    string Protocol { get; set; }
    string Scheme { get; set; }
    string Method { get; set; }
    string PathBase { get; set; }
    string Path { get; set; }
    string QueryString { get; set; }
    string RawTarget { get; set; }
    IHeaderDictionary Headers { get; set; }
    Stream Body { get; set; }
}

再看一下表单特性的定义:

public interface IFormFeature
{
    bool HasFormContentType { get; }
    IFormCollection Form { get; set; }
    IFormCollection ReadForm();
    Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken);
}

可以看到,这些特性对象与我们熟悉的 HttpContext 中的属性非常相似,这也就大大简化了在 IHttpRequestFeatureHttpContext 之间的转换。我们可以通过这些特性接口定义的属性来获取到原始上下文中描述的信息,并通过特性对象提供的方法来操作原始上下文,它就像Web Server与我们的应用程序之间的桥梁,完成抽象和具体之间的转换。

ASP.NET Core 提供了一系列丰富的特性对象,如 Session, Cookies, Query, Form, WebSocket, Request, Response 等等, 更详细的列表可以查看 Microsoft.AspNetCore.Http.Features

HttpContext

HttpContext 对象我们应该都很熟悉了,它用来表示一个抽象的HTTP上下文,而HttpContext对象的核心又体现在用于描述请求的Request和描述响应的Response属性上。除此之外,它还包含一些与当前请求相关的其他上下文信息,如描述当前HTTP连接的ConnectionInfo对象,控制WebSocket的WebSocketManager,代表当前用户的ClaimsPrincipal对象的Session,等等:

public abstract class HttpContext
{
    public abstract IFeatureCollection Features { get; }
    public abstract HttpRequest Request { get; }
    public abstract HttpResponse Response { get; }
    public abstract ConnectionInfo Connection { get; }
    public abstract WebSocketManager WebSockets { get; }
    public abstract ClaimsPrincipal User { get; set; }
    public abstract IDictionary<object, object> Items { get; set; }
    public abstract IServiceProvider RequestServices { get; set; }
    public abstract CancellationToken RequestAborted { get; set; }
    public abstract string TraceIdentifier { get; set; }
    public abstract ISession Session { get; set; }
    public abstract void Abort();
}

在我们处理请求时,如果希望终止该请求,可以通过 RequestAborted 属性给请求管道发送一个终止信息。当需要对整个管道共享一些与当前上下文相关的数据,可以将它保存在 Items 字典中。而在 ASP.NET Coer 1.x 中还包含一个管理认证的AuthenticationManager对象,但是在 2.0 中,将它移到了 AuthenticationHttpContextExtensions 中,因为用户认证本来就一个相对复杂且独立的模块,把它独立出去会更加符合 ASP.NET Core 的简洁模块化特性。

在上文中,我们了解到 HttpContext 的默认实现使用的是 DefaultHttpContext 类型 ,而 DefaultHttpContext 便是对上面介绍的 IFeatureCollection 对象的封装:

public class DefaultHttpContext : HttpContext
{
    private FeatureReferences<FeatureInterfaces> _features;

    private HttpRequest _request;
    private HttpResponse _response;

    public DefaultHttpContext(IFeatureCollection features)
    {
        Initialize(features);
    }

    public virtual void Initialize(IFeatureCollection features)
    {
        _features = new FeatureReferences<FeatureInterfaces>(features);
        _request = InitializeHttpRequest();
        _response = InitializeHttpResponse();
    }

    protected virtual HttpRequest InitializeHttpRequest() => new DefaultHttpRequest(this);
}

如上,DefaultHttpContext通过 Initialize 来完成从 IFeatureCollection 到 HttpContext 的转换,而各个属性的转换又交给了它们自己。

HttpRequest

HttpRequest 可以用来获取到描述当前请求的各种相关信息,比如请求的协议(HTTP或者HTTPS)、HTTP方法、地址,以及该请求的请求头,请求体等:

public abstract class HttpRequest
{
    public abstract HttpContext HttpContext { get; }
    public abstract string Method { get; set; }
    public abstract string Scheme { get; set; }
    public abstract bool IsHttps { get; set; }
    public abstract HostString Host { get; set; }
    public abstract PathString PathBase { get; set; }
    public abstract PathString Path { get; set; }
    public abstract QueryString QueryString { get; set; }
    public abstract IQueryCollection Query { get; set; }
    public abstract string Protocol { get; set; }
    public abstract IHeaderDictionary Headers { get; }
    public abstract IRequestCookieCollection Cookies { get; set; }
    public abstract long? ContentLength { get; set; }
    public abstract string ContentType { get; set; }
    public abstract Stream Body { get; set; }
    public abstract bool HasFormContentType { get; }
    public abstract IFormCollection Form { get; set; }
    public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken());
}

HttpRequest是一个抽象类,它的默认实现是DefaultHttpRequest:

public class DefaultHttpRequest : HttpRequest
{
    private readonly static Func<IFeatureCollection, IHttpRequestFeature> _nullRequestFeature = f => null;
    private FeatureReferences<FeatureInterfaces> _features;
    
    public DefaultHttpRequest(HttpContext context)
    {
        Initialize(context);
    }

    public virtual void Initialize(HttpContext context)
    {
        _context = context;
        _features = new FeatureReferences<FeatureInterfaces>(context.Features);
    }

    private IHttpRequestFeature HttpRequestFeature => _features.Fetch(ref _features.Cache.Request, _nullRequestFeature);

    public override string Method
    {
        get { return HttpRequestFeature.Method; }
        set { HttpRequestFeature.Method = value; }
    }

    public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
    {
        return FormFeature.ReadFormAsync(cancellationToken);
    }
}

在 DefaultHttpRequest 中,并没有额外的功能,它只是简单的与 IHttpRequestFeature 中的同名属性和方法做了一个映射,而 IHttpRequestFeature 对象的获取又涉及到一个 FeatureReferences<FeatureInterfaces> 类型, 从字面意思来说,就是对Feature对象的一个引用,用来保存对应的Feature实例,并在上文介绍的 Revision 属性发生变化时,清空Feature实例的缓存:

public struct FeatureReferences<TCache>
{
    public IFeatureCollection Collection { get; private set; }
    public int Revision { get; private set; }

    public TCache Cache;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public TFeature Fetch<TFeature, TState>(ref TFeature cached, TState state, Func<TState, TFeature> factory) where TFeature : class
    {
        var flush = false;
        var revision = Collection.Revision;
        if (Revision != revision)
        {
            cached = null;
            flush = true;
        }
        return cached ?? UpdateCached(ref cached, state, factory, revision, flush);
    }

    private TFeature UpdateCached<TFeature, TState>(ref TFeature cached, TState state, Func<TState, TFeature> factory, int revision, bool flush) where TFeature : class
    {
        if (flush)
        {
            Cache = default(TCache);
        }
        cached = Collection.Get<TFeature>();
        if (cached == null)
        {
            cached = factory(state);
            Collection.Set(cached);
            Revision = Collection.Revision;
        }
        else if (flush)
        {
            Revision = revision;
        }
        return cached;
    }

    public TFeature Fetch<TFeature>(ref TFeature cached, Func<IFeatureCollection, TFeature> factory)
        where TFeature : class => Fetch(ref cached, Collection, factory);
}

如上,当 Revision 生成变化时,会将 Cache 设置为 null , 然后重新从 IFeatureCollection 中获取,最后更新 Revision 为最新版本,相当于一个缓存工厂。

Fetch方法使用了[MethodImpl(MethodImplOptions.AggressiveInlining)]特性,表示该方法会尽可能的使用内联方式来执行。而内联是一种很重要的优化方式, 它允许编译器在方法调用开销比方法本身更大的情况下消除对方法调用的开销,即直接将该方法体嵌入到调用者中。

HttpResponse

在了解了表示请求的抽象类 HttpRequest 之后,我们再来认识一下与它对应的,用来描述响应的 HttpResponse 类型:

public abstract class HttpResponse
{
    private static readonly Func<object, Task> _callbackDelegate = callback => ((Func<Task>)callback)();
    private static readonly Func<object, Task> _disposeDelegate = disposable =>
    {
        ((IDisposable)disposable).Dispose();
        return Task.CompletedTask;
    };

    public abstract HttpContext HttpContext { get; }
    public abstract int StatusCode { get; set; }
    public abstract IHeaderDictionary Headers { get; }
    public abstract Stream Body { get; set; }
    public abstract long? ContentLength { get; set; }
    public abstract string ContentType { get; set; }
    public abstract IResponseCookies Cookies { get; }
    public abstract bool HasStarted { get; }
    public abstract void OnStarting(Func<object, Task> callback, object state);
    public virtual void OnStarting(Func<Task> callback) => OnStarting(_callbackDelegate, callback);
    public abstract void OnCompleted(Func<object, Task> callback, object state);
    public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable);
    public virtual void OnCompleted(Func<Task> callback) => OnCompleted(_callbackDelegate, callback);
    public virtual void Redirect(string location) => Redirect(location, permanent: false);
    public abstract void Redirect(string location, bool permanent);
}

HttpResponse也是一个抽象类,我们使用它来输出对请求的响应,如设置HTTP状态码,Cookies,HTTP响应报文头,响应主体等,以及提供了一些将响应发送到客户端时的相关事件。

HasStarted 属性用来表示响应是否已开始发往客户端,在我们第一次调用 response.Body.WriteAsync 方法时,该属性便会被设置为 True。需要注意的是,一旦 HasStarted 设置为 true 后,便不能再修改响应头,否则将会抛出 InvalidOperationException 异常,也建议我们在HasStarted设置为true后,不要再对 Response 进行写入,因为此时 content-length 的值已经确定,继续写入可能会造成协议冲突。

HttpResponse 的默认实现为 DefaultHttpResponse ,它与 DefaultHttpRequest 类似,只是对 IHttpResponseFeature 的封装,不过 ASP.NET Core 也为我们提供了一些扩展方法,如:我们在写入响应时,通常使用的是 Response 的扩展方法 WriteAsync

public static class HttpResponseWritingExtensions
{
    public static Task WriteAsync(this HttpResponse response, string text, CancellationToken cancellationToken = default(CancellationToken))
    {
        return response.WriteAsync(text, Encoding.UTF8, cancellationToken);
    }

    public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken))
    {
        byte[] data = encoding.GetBytes(text);
        return response.Body.WriteAsync(data, 0, data.Length, cancellationToken);
    }
}

ASP.NET Core 还为 Response 提供了用来一个清空响应头和响应体的扩展方法:

public static class ResponseExtensions
{
    public static void Clear(this HttpResponse response)
    {
        if (response.HasStarted)
        {
            throw new InvalidOperationException("The response cannot be cleared, it has already started sending.");
        }
        response.StatusCode = 200;
        response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = null;
        response.Headers.Clear();
        if (response.Body.CanSeek)
        {
            response.Body.SetLength(0);
        }
    }
}

还有比较常用的发送文件的扩展方法:SendFileAsync ,获取响应头的扩展方法:GetTypedHeaders 等等,就不再细说。

IHttpContextAccessor

在 ASP.NET 4.x 我们经常会通过 HttpContext.Current 来获取当前请求的 HttpContext 对象,而在 ASP.NET Core 中,HttpContext 不再有 Current 属性,并且在 ASP.NET Core 中一切皆注入,更加推荐使用注入的方式来获取实例,而非使用静态变量。因此,ASP.NET Core 提供了一个 IHttpContextAccessor 接口,用来统一获取当前请求的 HttpContext 实例的方式:

public interface IHttpContextAccessor
{
    HttpContext HttpContext { get; set; }
}

它的定义非常简单,就只有一个 HttpContext 属性,它在ASP.NET Core 中还有一个内置的实现类:HttpContextAccessor

public class HttpContextAccessor : IHttpContextAccessor
{
    private static AsyncLocal<HttpContext> _httpContextCurrent = new AsyncLocal<HttpContext>();

    public HttpContext HttpContext
    {
        get
        {
            return _httpContextCurrent.Value;
        }
        set
        {
            _httpContextCurrent.Value = value;
        }
    }
}

这里使用了一个 AsyncLocal<T> 类型来保存 HttpContext 对象,可能很多人对 AsyncLocal 不太了解,这里就来介绍一下:

在.NET 4.5 中引用了 async await 等关键字,使我们可以像编写同步方法一样方便的来执行异步操作,因此我们的大部分代码都会使用异步。以往我们所使用的 ThreadLocal 在同步方法中没有问题,但是在 await 后有可能会创建新实的例(await 之后可能还交给之前的线程执行,也有可能是一个新的线程来执行),而不再适合用来保存线程内的唯一实例,因此在 .NET 4.6 中引用了 AsyncLocal<T> 类型,它类似于 ThreadLocal,但是在 await 之后就算切换线程也仍然可以保持同一实例。我们知道在 ASP.NET 4.x 中,HttpContext的 Current 实例是通过 CallContext 对象来保存的,但是 ASP.NET Core 中不再支持CallContext,故使用 AsyncLocal<T> 来保证线程内的唯一实例。

不过,ASP.NET Core 默认并没有注入 IHttpContextAccessor 对象,如果我们想在应用程序中使用它,则需要手动来注册:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

在上面介绍的HttpContextFactory类的构造函数中会注入IHttpContextAccessor实例,并为其HttpContext属性赋值,并在Dispose方法中将其设置为null。

总结

在ASP.NET 4.x 中,我们就对 HttpContext 非常熟悉了,而在 ASP.NET Core 中,它的变化并不大,只是做了一些简化,因此本文较为简单,主要描述了一下 HttpContext 是如何创建的,以及它的构成,最后则介绍了一下在每个请求中获取 HttpContext 唯一实例的方式,而在 ASP.NET Core 2.0 中 HttpContext 的 AuthenticationManager 对象已标记为过时,添加了一些扩展方法来实现AuthenticationManager中的功能,下一章就来介绍一下 ASP.NET Core 中的认证系统。

posted @ 2017-09-08 08:34  雨夜朦胧  阅读(13517)  评论(7编辑  收藏  举报