Dot-Net

如何在依賴於域的 .NET Core 中更改/創建自定義 FileProvider(即一個 Web 應用程序服務於多個站點呈現邏輯)

  • March 4, 2020

我目前正在使用 .NET Core 創建一個多租戶 Web 應用程序。並且面臨一個問題:

1)Web App基於一組域名提供不同的****視圖邏輯

  1. 視圖是 MVC 視圖並儲存在Azure Blob 儲存中

  2. 多個站點共享相同的 .NET Core MVC 控制器,因此只有 Razor 視圖在邏輯上有所不同。

問題…. A) 這可能嗎?我創建了一個 MiddleWare 來進行操作,但是我無法在上下文級別正確分配 FileProviders,因為文件提供程序應該依賴於域。

B)或者,除了通過 FileProvider 思考和嘗試之外,還有其他方法可以實現我想要實現的目標嗎?

非常感謝!!!

您描述的任務並不簡單。這裡的主要問題不是獲取最新的HttpContext,它可以很容易地完成IHttpContextAccessor。您將面臨的主要障礙是 Razor View Engine 大量使用記憶體。

壞消息是請求域名不是這些記憶體中鍵的一部分,只有視圖子路徑屬於一個鍵。因此,如果您請求一個帶有/Views/Home/Index.cshtmldomain1 子路徑的視圖,它將被載入、編譯和記憶體。然後,您請求一個具有相同路徑但在 domain2 內的視圖。您希望獲得另一個特定於 domain2 的視圖,但 Razor 不在乎,它甚至不會呼叫您的 custom FileProvider,因為將使用記憶體的視圖。

Razor 基本上使用了 2 個記憶體:

第一個ViewLookupCacheRazorViewEngine中聲明為:

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 的第一個記憶體層MultiTenantRazorViewEngineMultiTenantMemoryCache

MultiTenantRazorPageFactoryProvider 類

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並且對於這些初始呼叫返回IFileInfoset Existsto false。或者保留PhysicalFileProvider在 RazorFileProviders集合中,以便所有域共享這些文件。在我的範例中,我使用了以前的方法。

配置(啟動類)

ConfigureServices()方法中,我們應該:

  1. IRazorViewEngine用替換實現MultiTenantRazorViewEngine
  2. IViewCompilerProvider用 MultiTenantRazorViewEngine替換實現。
  3. IRazorPageFactoryProvider用替換實現MultiTenantRazorPageFactoryProvider
  4. 清除 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()方法中,我們應該:

  1. 設置 的實例MultiTenantHelper.ServiceProvider
  2. 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?}");
   });
}

GitHub 上的範例項目

引用自:https://stackoverflow.com/questions/47313612