全域性獲取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>();
}
.....
}