全域性獲取HttpContext

是你晨曦哥呀發表於2021-07-11

全域性獲取HttpContext

在我們平常開發中會有這樣的需求,我們的Service業務層需要獲取請求上下文中的使用者資訊,一般我們從控制器引數傳遞過來。如果你覺得這樣就可以了,請您關閉文章。

場景

但是我們也會遇到控制器傳遞困難的場景,我自己最近使用單庫實現多租戶的PAAS平臺,發現EF Core上下文獲取我Token或者Headers中獲取租戶Id進行全域性過濾就很麻煩(多租戶解決方案後期我補充)。

涉及知識

我們先要知道一個思想如果想要整個.NET程式中共享一個變數,我們可以將想要共享的變數放在某個類的靜態屬性上來實現。
但是我們的請求上下文每個人的資訊不一樣,就需要將這個變數的共享範圍縮小到單個執行緒內。例如在web應用中,伺服器為每個同時訪問的請求分配一個獨立的執行緒,我們要在這些獨立的執行緒中維護自己的當前訪問使用者的資訊時,就需要需要執行緒本地儲存了。

  • IHttpContextAccessor 設定實現規範
  • HttpContextAccessor 基於當前執行上下文提供的實現。
  • AsyncLocal 實現多執行緒中靜態變數獨立化 (這裡畫一個圈圈)

這個時候我們再看原始碼思路就清晰了,我們通過注入HttpContextAccessor,然後內部將請求上下文儲存在_httpContextCurrent靜態變數中,這個就可以全域性訪問啦(當然訪問範圍是在該主執行緒內部)。

    // HttpContextAccessor原始碼
    public class HttpContextAccessor : IHttpContextAccessor
    {
        // 通過AsyncLocal儲存當前上下文資訊
        private static readonly AsyncLocal<HttpContextHolder> _httpContextCurrent =new AsyncLocal<HttpContextHolder>();

        public HttpContext? HttpContext
        {
            get
            {
                return  _httpContextCurrent.Value?.Context;
            }
            set
            {
                var holder = _httpContextCurrent.Value;
                if (holder != null)
                {
                    // 清除AsyncLocals中捕獲的當前HttpContext
                    holder.Context = null;
                }
                if (value != null)
                {
                    // 使用一個物件間接在AsyncLocal中儲存HttpContext,
                    // 所以當它被清除時,它可以在所有的ExecutionContexts中被清除。
                    _httpContextCurrent.Value = new HttpContextHolder { Context = 
value };
                }
            }
        }

        private class HttpContextHolder
        {
            public HttpContext? Context;
        }

整活

首先我們需要在Startup的ConfigureServices方法中註冊IHttpContextAccessor的例項

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

這個時候你Service層注入該類的時候就可以獲取到請求上下文資訊了,但是這個就不符合我們詩一般程式設計師的氣質。
因為直接將請求上下文字丟擲來還挺多的,我們本來只需要租戶ID但是你給我一坨,挺不好把握的。

整大活

我們可以進行包裝,我使用PrincipalAccessor進行請求上下文拆解

然後在Startup的ConfigureServices方法中,我們一樣把這個類也加入註冊中

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

最後自己專案的一些優化

自己不斷的在優化自己的專案結構,或者設計思路,我發現我為什麼有這麼多注入,我建構函式都要爆了。

然後自己想了想,我其實可以將訪問上下文的類放入BaseService中靜態變數儲存,系統提供了IServiceCollection來註冊服務和提供了IServiceProvider這個讓我們解析各種註冊過的服務.
我們定義一個儲存類

public class ServiceProviderInstance
 {
      public static IServiceProvider Instance { get; set; }
 } 
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
 {
        ...
       ServiceProviderInstance.Instance = app.ApplicationServices;
 }

寶貝相信我剩下的我們交給時間,我們只需要這樣(BaseService定義屬性、獲取注入就可以了),然後就那樣(就直接可以使用啦)

    public class BaseService<T, Repository> : IBaseService<T>
          where T : BaseEntityCore, new()
          //規定這個Repository型別一定是繼承倉儲的介面,下面就可以使用介面的方法
          where Repository : IBaseRepository<T>
    {
        /// <summary>
        /// 身份資訊
        /// </summary>
        protected IClaimsAccessor Claims { get; set; }

        /// <summary>
        /// 獲取倉儲實體
        /// </summary>
        private readonly Repository CurrentRepository;

        public BaseService(Repository currentRepository)
        {
            CurrentRepository = currentRepository;
            Claims = ServiceProviderInstance.Instance.GetRequiredService<IClaimsAccessor>();
        }
        .....
}

相關文章