如何在依賴於域的 .NET Core 中更改/創建自定義 FileProvider(即一個 Web 應用程序服務於多個站點呈現邏輯)
我目前正在使用 .NET Core 創建一個多租戶 Web 應用程序。並且面臨一個問題:
1)Web App基於一組域名提供不同的****視圖和邏輯。
視圖是 MVC 視圖並儲存在Azure Blob 儲存中
多個站點共享相同的 .NET Core MVC 控制器,因此只有 Razor 視圖在邏輯上有所不同。
問題…. A) 這可能嗎?我創建了一個 MiddleWare 來進行操作,但是我無法在上下文級別正確分配 FileProviders,因為文件提供程序應該依賴於域。
B)或者,除了通過 FileProvider 思考和嘗試之外,還有其他方法可以實現我想要實現的目標嗎?
非常感謝!!!
您描述的任務並不簡單。這裡的主要問題不是獲取最新的
HttpContext,它可以很容易地完成IHttpContextAccessor。您將面臨的主要障礙是 Razor View Engine 大量使用記憶體。壞消息是請求域名不是這些記憶體中鍵的一部分,只有視圖子路徑屬於一個鍵。因此,如果您請求一個帶有
/Views/Home/Index.cshtmldomain1 子路徑的視圖,它將被載入、編譯和記憶體。然後,您請求一個具有相同路徑但在 domain2 內的視圖。您希望獲得另一個特定於 domain2 的視圖,但 Razor 不在乎,它甚至不會呼叫您的 customFileProvider,因為將使用記憶體的視圖。Razor 基本上使用了 2 個記憶體:
第一個
ViewLookupCache在RazorViewEngine中聲明為:protected IMemoryCache ViewLookupCache { get; }嗯,事情越來越糟了。此屬性被聲明為非虛擬的並且沒有設置器。
RazorViewEngine因此,使用將域作為鍵的一部分的視圖記憶體進行擴展並不容易。RazorViewEngine註冊為單例並註入到PageResultExecutor也註冊為單例的類中。所以我們沒有辦法RazorViewEngine為每個域解析新實例,以便它有自己的記憶體。似乎解決此問題的最簡單方法是將屬性ViewLookupCache(儘管它沒有設置器)設置為IMemoryCache. 可以在沒有設置器的情況下設置屬性然而,這是一個非常骯髒的黑客。目前我向您提出這樣的解決方法,上帝殺死了一隻小貓。但是我沒有看到更好的繞過選項RazorViewEngine,它對於這種情況來說不夠靈活。第二個 Razor 記憶體
_precompiledViewLookup位於RazorViewCompiler中:private readonly Dictionary<string, CompiledViewDescriptor> _precompiledViews;這個記憶體儲存為私有欄位,但是我們可以
RazorViewCompiler為每個域創建一個新的實例,因為它是實例化的IViewCompilerProvider,我們可以通過它以多租戶的方式實現。所以記住這一切,讓我們做這項工作。
MultiTenantRazorViewEngine 類
public class MultiTenantRazorViewEngine : RazorViewEngine { public MultiTenantRazorViewEngine(IRazorPageFactoryProvider pageFactory, IRazorPageActivator pageActivator, HtmlEncoder htmlEncoder, IOptions<RazorViewEngineOptions> optionsAccessor, RazorProject razorProject, ILoggerFactory loggerFactory, DiagnosticSource diagnosticSource) : base(pageFactory, pageActivator, htmlEncoder, optionsAccessor, razorProject, loggerFactory, diagnosticSource) { // Dirty hack: setting RazorViewEngine.ViewLookupCache property that does not have a setter. var field = typeof(RazorViewEngine).GetField("<ViewLookupCache>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); field.SetValue(this, new MultiTenantMemoryCache()); // Asserting that ViewLookupCache property was set to instance of MultiTenantMemoryCache if (ViewLookupCache .GetType() != typeof(MultiTenantMemoryCache)) { throw new InvalidOperationException("Failed to set multi-tenant memory cache"); } } }
MultiTenantRazorViewEngine派生自RazorViewEngine並將ViewLookupCache屬性設置為 的實例MultiTenantMemoryCache。MultiTenantMemoryCache 類
public class MultiTenantMemoryCache : IMemoryCache { // Dictionary with separate instance of IMemoryCache for each domain private readonly ConcurrentDictionary<string, IMemoryCache> viewLookupCache = new ConcurrentDictionary<string, IMemoryCache>(); public bool TryGetValue(object key, out object value) { return GetCurrentTenantCache().TryGetValue(key, out value); } public ICacheEntry CreateEntry(object key) { return GetCurrentTenantCache().CreateEntry(key); } public void Remove(object key) { GetCurrentTenantCache().Remove(key); } private IMemoryCache GetCurrentTenantCache() { var currentDomain = MultiTenantHelper.CurrentRequestDomain; return viewLookupCache.GetOrAdd(currentDomain, domain => new MemoryCache(new MemoryCacheOptions())); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { foreach (var cache in viewLookupCache) { cache.Value.Dispose(); } } } }
MultiTenantMemoryCache``IMemoryCache是為不同域分離記憶體數據的一種實現。現在我們將域名添加到 Razor 的第一個記憶體層MultiTenantRazorViewEngine。MultiTenantMemoryCacheMultiTenantRazorPageFactoryProvider 類
public class MultiTenantRazorPageFactoryProvider : IRazorPageFactoryProvider { // Dictionary with separate instance of IMemoryCache for each domain private readonly ConcurrentDictionary<string, IRazorPageFactoryProvider> providers = new ConcurrentDictionary<string, IRazorPageFactoryProvider>(); public RazorPageFactoryResult CreateFactory(string relativePath) { var currentDomain = MultiTenantHelper.CurrentRequestDomain; var factoryProvider = providers.GetOrAdd(currentDomain, domain => MultiTenantHelper.ServiceProvider.GetRequiredService<DefaultRazorPageFactoryProvider>()); return factoryProvider.CreateFactory(relativePath); } }
MultiTenantRazorPageFactoryProvider創建單獨的實例,DefaultRazorPageFactoryProvider以便我們RazorViewCompiler為每個域都有一個不同的實例。現在我們已經將域名添加到 Razor 的第二個記憶體層。MultiTenantHelper 類
public static class MultiTenantHelper { public static IServiceProvider ServiceProvider { get; set; } public static HttpContext CurrentHttpContext => ServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext; public static HttpRequest CurrentRequest => CurrentHttpContext.Request; public static string CurrentRequestDomain => CurrentRequest.Host.Host; }
MultiTenantHelper提供對該請求的目前請求和該請求的域名的訪問。不幸的是,我們必須將其聲明為帶有靜態訪問器的靜態類IHttpContextAccessor。Razor 和靜態文件中間件都不允許FileProvider為每個請求設置新的實例(參見下面的Startup類)。這就是為什麼IHttpContextAccessor不注入FileProvider並作為靜態屬性訪問的原因。MultiTenantFileProvider 類
public class MultiTenantFileProvider : IFileProvider { private const string BasePath = @"DomainsData"; public IFileInfo GetFileInfo(string subpath) { if (MultiTenantHelper.CurrentHttpContext == null) { if (String.Equals(subpath, @"/Pages/_ViewImports.cshtml") || String.Equals(subpath, @"/_ViewImports.cshtml")) { // Return FileInfo of non-existing file. return new NotFoundFileInfo(subpath); } throw new InvalidOperationException("HttpContext is not set"); } return CreateFileInfoForCurrentRequest(subpath); } public IDirectoryContents GetDirectoryContents(string subpath) { var fullPath = GetPhysicalPath(MultiTenantHelper.CurrentRequestDomain, subpath); return new PhysicalDirectoryContents(fullPath); } public IChangeToken Watch(string filter) { return NullChangeToken.Singleton; } private IFileInfo CreateFileInfoForCurrentRequest(string subpath) { var fullPath = GetPhysicalPath(MultiTenantHelper.CurrentRequestDomain, subpath); return new PhysicalFileInfo(new FileInfo(fullPath)); } private string GetPhysicalPath(string tenantId, string subpath) { subpath = subpath.TrimStart(Path.AltDirectorySeparatorChar); subpath = subpath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); return Path.Combine(BasePath, tenantId, subpath); } }此實現
MultiTenantFileProvider僅用於範例。你應該把你的實現基於 Azure Blob 儲存。您可以通過呼叫獲取目前請求的域名MultiTenantHelper.CurrentRequestDomain。您應該準備好在GetFileInfo()應用程序啟動期間app.UseMvc()呼叫該方法。它發生在導入所有其他視圖使用的命名空間的文件中/Pages/_ViewImports.cshtml。/_ViewImports.cshtml由於GetFileInfo()不在任何請求中呼叫,IHttpContextAccessor.HttpContext因此將返回null. 因此,您應該為每個域擁有自己的副本,_ViewImports.cshtml並且對於這些初始呼叫返回IFileInfosetExiststofalse。或者保留PhysicalFileProvider在 RazorFileProviders集合中,以便所有域共享這些文件。在我的範例中,我使用了以前的方法。配置(啟動類)
在
ConfigureServices()方法中,我們應該:
IRazorViewEngine用替換實現MultiTenantRazorViewEngine。IViewCompilerProvider用 MultiTenantRazorViewEngine替換實現。IRazorPageFactoryProvider用替換實現MultiTenantRazorPageFactoryProvider。- 清除 Razor 的
FileProviders集合併添加自己的MultiTenantFileProvider.public void ConfigureServices(IServiceCollection services) { services.AddMvc(); var fileProviderInstance = new MultiTenantFileProvider(); services.AddSingleton(fileProviderInstance); services.AddSingleton<IRazorViewEngine, MultiTenantRazorViewEngine>(); // Overriding singleton registration of IViewCompilerProvider services.AddTransient<IViewCompilerProvider, RazorViewCompilerProvider>(); services.AddTransient<IRazorPageFactoryProvider, MultiTenantRazorPageFactoryProvider>(); // MultiTenantRazorPageFactoryProvider resolves DefaultRazorPageFactoryProvider by its type services.AddTransient<DefaultRazorPageFactoryProvider>(); services.Configure<RazorViewEngineOptions>(options => { // Remove instance of PhysicalFileProvider options.FileProviders.Clear(); options.FileProviders.Add(fileProviderInstance); }); }在
Configure()方法中,我們應該:
- 設置 的實例
MultiTenantHelper.ServiceProvider。- 將
FileProvider靜態文件中間件設置為MultiTenantFileProvider.public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } MultiTenantHelper.ServiceProvider = app.ApplicationServices.GetRequiredService<IServiceProvider>(); app.UseStaticFiles(new StaticFileOptions { FileProvider = app.ApplicationServices.GetRequiredService<MultiTenantFileProvider>() }); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }