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

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之外,还有其他方法可以实现我想要实现的目标吗?

非常感谢!!!

Cod*_*ler 9

您描述的任务不是很简单。这里的主要问题是没有电流HttpContext,可以很容易地做到IHttpContextAccessor。您将面临的主要障碍是Razor View Engine大量使用了缓存。

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

Razor基本上使用2种缓存:

第一个是ViewLookupCacheRazorViewEngine中声明为:

protected IMemoryCache ViewLookupCache { get; }
Run Code Online (Sandbox Code Playgroud)

好吧,事情变得越来越糟。此属性被声明为非虚拟的,并且没有setter。因此,要扩展RazorViewEngine具有域作为键的一部分的视图缓存并不是一件容易的事。RazorViewEngine被注册为单例并注入到PageResultExecutor也注册为单例的类中。因此,我们没有RazorViewEngine为每个域解析新实例的方法,因此它具有自己的缓存。解决此问题的最简单方法似乎是将属性ViewLookupCache(尽管它没有设置器)设置为的多租户实现IMemoryCache可以不使用setter 设置属性但是,这是一个非常肮脏的技巧。目前,我向您提出这样的解决方法,上帝杀死了一只小猫。但是,我看不到绕过的更好选择RazorViewEngine,但是对于这种情况而言,它不够灵活。

第二个Razor缓存_precompiledViewLookupRazorViewCompiler中

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不同域的缓存数据分开的实现。现在,使用MultiTenantRazorViewEngineMultiTenantMemoryCache将域名添加到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为每个域,并为这些初始调用返回IFileInfoExists设置为false。或者保留PhysicalFileProvider在Razor FileProviders集合中,以便所有域都可以共享这些文件。在我的示例中,我使用了以前的方法。

配置(启动类)

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

  1. 替换实施IRazorViewEngineMultiTenantRazorViewEngine
  2. IViewCompilerProvider用MultiTenantRazorViewEngine 替换实现。
  3. 替换实施IRazorPageFactoryProviderMultiTenantRazorPageFactoryProvider
  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);
    });
}
Run Code Online (Sandbox Code Playgroud)

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?}");
    });
}
Run Code Online (Sandbox Code Playgroud)

GitHub上的示例项目