Lid*_*eng 5 .net asp.net-core-mvc .net-core asp.net-core
我目前正在使用.NET Core创建一个多租户Web应用程序。并面临一个问题:
1)Web App 基于一组域名提供不同的视图和逻辑。
2)视图是MVC视图,并存储在Azure Blob存储中
3)多个站点共享相同的.NET Core MVC控制器,因此只有Razor视图在小的逻辑上是不同的。
问题...。A)可能吗?我创建了一个MiddleWare来进行操作,但是由于文件提供者应依赖于域,因此无法在上下文级别正确分配文件提供者。
B)或者,除了思考和尝试通过FileProvider之外,还有其他方法可以实现我想要实现的目标吗?
非常感谢!!!
您描述的任务不是很简单。这里的主要问题是没有电流HttpContext,可以很容易地做到IHttpContextAccessor。您将面临的主要障碍是Razor View Engine大量使用了缓存。
坏消息是请求域名不是这些缓存中键的一部分,只有视图子路径属于键。因此,如果您请求带有/Views/Home/Index.cshtmldomain1 子路径的视图,它将被加载,编译和缓存。然后,您请求具有相同路径但在domain2内的视图。您希望获得另一个针对domain2的视图,但是Razor不在乎,它甚至不会调用您的custom FileProvider,因为将使用缓存的视图。
Razor基本上使用2种缓存:
第一个是ViewLookupCache在RazorViewEngine中声明为:
protected IMemoryCache ViewLookupCache { get; }
Run Code Online (Sandbox Code Playgroud)
好吧,事情变得越来越糟。此属性被声明为非虚拟的,并且没有setter。因此,要扩展RazorViewEngine具有域作为键的一部分的视图缓存并不是一件容易的事。RazorViewEngine被注册为单例并注入到PageResultExecutor也注册为单例的类中。因此,我们没有RazorViewEngine为每个域解析新实例的方法,因此它具有自己的缓存。解决此问题的最简单方法似乎是将属性ViewLookupCache(尽管它没有设置器)设置为的多租户实现IMemoryCache。可以不使用setter 来设置属性但是,这是一个非常肮脏的技巧。目前,我向您提出这样的解决方法,上帝杀死了一只小猫。但是,我看不到绕过的更好选择RazorViewEngine,但是对于这种情况而言,它不够灵活。
第二个Razor缓存_precompiledViewLookup在RazorViewCompiler中:
private readonly Dictionary<string, CompiledViewDescriptor> _precompiledViews;
Run Code Online (Sandbox Code Playgroud)
此缓存存储为私有字段,但是我们可以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");
}
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
MultiTenantMemoryCache是将IMemoryCache不同域的缓存数据分开的实现。现在,使用MultiTenantRazorViewEngine和MultiTenantMemoryCache将域名添加到Razor的第一个缓存层。
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);
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
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);
}
}
Run Code Online (Sandbox Code Playgroud)
的此实现MultiTenantFileProvider仅用于示例。您应基于Azure Blob存储放置实施。您可以通过调用来获取当前请求的域名MultiTenantHelper.CurrentRequestDomain。您应该已经准备好在GetFileInfo()从app.UseMvc()调用启动应用程序期间调用该方法。对于/Pages/_ViewImports.cshtml和/_ViewImports.cshtml会导入所有其他视图使用的名称空间的文件,会发生这种情况。由于GetFileInfo()未在任何请求内调用,IHttpContextAccessor.HttpContext将返回null。所以,你应该要么有自己的副本,_ViewImports.cshtml为每个域,并为这些初始调用返回IFileInfo与Exists设置为false。或者保留PhysicalFileProvider在Razor FileProviders集合中,以便所有域都可以共享这些文件。在我的示例中,我使用了以前的方法。
配置(启动类)
在ConfigureServices()方法中,我们应该:
IRazorViewEngine有MultiTenantRazorViewEngine。IViewCompilerProvider用MultiTenantRazorViewEngine 替换实现。IRazorPageFactoryProvider有MultiTenantRazorPageFactoryProvider。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);
});
}
Run Code Online (Sandbox Code Playgroud)
在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?}");
});
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1233 次 |
| 最近记录: |