如何在依赖于域的 .NET Core 中更改/创建自定义文件提供程序(即一个 Web 应用提供多个站点呈现逻辑)



我目前正在使用.NET Core创建一个多租户web应用程序。并面临一个问题:

1) Web应用程序基于一组域名提供不同的视图逻辑

2) 这些视图是MVC视图,存储在Azure Blob存储

3) 多个站点共享相同的.NET Core MVC控制器,因此只有Razor视图与小逻辑不同。

问题。。。。A) 这可能吗?我创建了一个MiddleWare来操作,但我无法在上下文级别正确分配FileProviders,因为文件提供程序应该依赖于域。

B) 或者,除了通过FileProvider思考和尝试,还有其他方法可以实现我想要实现的目标吗?

非常感谢!!!

您描述的任务并不简单。这里的主要问题不是获得当前的HttpContext,使用IHttpContextAccessor可以很容易地完成。您将面临的主要障碍是Razor视图引擎大量使用缓存。

坏消息是,请求域名不是这些缓存中密钥的一部分,只有视图子路径属于密钥。因此,如果您为domain1请求子路径为/Views/Home/Index.cshtml的视图,它将被加载、编译和缓存。然后,您请求一个具有相同路径但位于domain2内的视图。您希望获得另一个特定于domain2的视图,但Razor并不在乎,它甚至不会调用您的自定义FileProvider,因为将使用缓存的视图。

Razor基本上有两个缓存:

第一个是RazorViewEngine中的ViewLookupCache,声明为:

protected IMemoryCache ViewLookupCache { get; }

情况越来越糟了。此属性被声明为非虚拟属性,并且没有setter。因此,使用将域作为密钥一部分的视图缓存来扩展RazorViewEngine并不容易。CCD_ 7被注册为singleton,并且被注入到CCD_。因此,我们没有办法为每个域解析RazorViewEngine的新实例,因此它有自己的缓存。似乎解决这个问题最简单的方法是将属性ViewLookupCache(尽管它没有setter)设置为IMemoryCache的多租户实现。在没有setter的情况下设置属性是可能的,但这是一个非常肮脏的黑客。就在我向你提出这种变通办法的时候,上帝杀死了一只小猫。然而,我看不到绕过RazorViewEngine的更好选择,它对于这种情况来说不够灵活。

第二个Razor缓存是RazorViewCompiler:中的_precompiledViewLookup

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");
}
}
}

CCD_ 16从CCD_ 17派生并将CCD_ 18属性设置为CCD_ 19的实例。

多租户内存缓存类

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();
}
}
}
}

CCD_ 20是CCD_ 21的一种实现,用于分离不同域的缓存数据。现在使用MultiTenantRazorViewEngineMultiTenantMemoryCache,我们将域名添加到Razor的第一个缓存层。

MultiTenantAzorPageFactoryProvider类

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获取当前请求的域名。您应该准备好在应用程序启动期间从app.UseMvc()调用GetFileInfo()方法。这种情况发生在/Pages/_ViewImports.cshtml/_ViewImports.cshtml文件中,它们导入所有其他视图使用的名称空间。由于GetFileInfo()不在任何请求中被调用,因此IHttpContextAccessor.HttpContext将返回null。因此,对于每个域,您应该有自己的_ViewImports.cshtml副本,并且对于这些初始调用,返回IFileInfoExists设置为false。或者将PhysicalFileProvider保留在RazorFileProviders集合中,以便所有域都可以共享这些文件。在我的样本中,我使用了以前的方法。

配置(启动类)

ConfigureServices()方法中,我们应该:

  1. MultiTenantRazorViewEngine替换IRazorViewEngine的实现
  2. 用MultiTenantRazorViewEngine替换IViewCompilerProvider的实现
  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上的示例项目

您可以在本文中查看Saaskit,了解多租户

https://benfoster.io/blog/asp-net-core-themes-and-multi-tenancy

要查找视图,可以遵循@CodeFuller的概念,重写MultiTenantFileProvider

但在中,您必须将其注册为以下

services.AddRazorPages().AddRazorRuntimeCompilation(options =>
{
options.FileProviders.Clear();
options.FileProviders.Add(new TenantFileProvider());
});

相关内容

最新更新