gpt4 book ai didi

.net - 如何在域相关的 .NET Core 中更改/创建自定义 FileProvider(即一个 Web 应用程序为多个站点呈现逻辑提供服务)

转载 作者:行者123 更新时间:2023-12-02 02:34:45 27 4
gpt4 key购买 nike

我目前正在使用 .NET Core 创建 Multi-Tenancy Web 应用程序。并且正面临一个问题:

1) Web App 服务不同 浏览次数 逻辑 基于一组域名 .

2) View 是MVC View 并存储在中Azure Blob 存储

3)多个站点共享相同的 .NET Core MVC Controller 因此只有 Razor View 与小逻辑不同。

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

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

非常感谢!!!

最佳答案

你描述的任务不是很简单。这里的主要问题是不获取当前 HttpContext ,这可以通过 IHttpContextAccessor 轻松完成.您将面临的主要障碍是 Razor View Engine 大量使用缓存。

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

Razor 基本上使用了 2 个缓存:

第一个是 ViewLookupCacheRazorViewEngine声明为:

protected IMemoryCache ViewLookupCache { get; }

嗯,事情越来越糟了。此属性被声明为非虚拟的,并且没有 setter 。所以扩展 RazorViewEngine不是很容易使用具有域作为 key 一部分的 View 缓存。 RazorViewEngine注册为单例并注入(inject) PageResultExecutor类也注册为单例。所以我们没有办法解析 RazorViewEngine 的新实例为每个域,以便它有自己的缓存。
似乎这个问题最简单的解决方法是设置属性 ViewLookupCache (尽管它没有 setter )到 IMemoryCache 的 Multi-Tenancy 实现.不使用 setter 设置属性 is possible然而,这是一个非常肮脏的黑客。在我向您提出这种解决方法的那一刻,上帝杀死了一只小猫。但是我没有看到更好的选择来绕过 RazorViewEngine ,对于这种情况,它不够灵活。

第二个 Razor 缓存是 _precompiledViewLookupRazorViewCompiler :
private readonly Dictionary<string, CompiledViewDescriptor> _precompiledViews;

这个缓存被存储为私有(private)字段,但是我们可以有 RazorViewCompiler 的新实例对于每个域,因为它是由 IViewCompilerProvider 定义的我们可以以 Multi-Tenancy 的方式实现。

因此,请记住所有这些,让我们开始工作。

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();
}
}
}
}
MultiTenantMemoryCacheIMemoryCache 的实现将不同域的缓存数据分开。现在与 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);
}
}
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未注入(inject) 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导入所有其他 View 使用的命名空间的文件。自 GetFileInfo()不在任何请求中调用, IHttpContextAccessor.HttpContext将返回 null .所以你应该有自己的 _ViewImports.cshtml 副本对于每个域和这些初始调用返回 IFileInfoExists设置为 false .或保留 PhysicalFileProvider在 Razor FileProviders集合,以便所有域都可以共享这些文件。在我的示例中,我使用了以前的方法。

配置(启动类)

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

    Sample Project on GitHub

    关于.net - 如何在域相关的 .NET Core 中更改/创建自定义 FileProvider(即一个 Web 应用程序为多个站点呈现逻辑提供服务),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47313612/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com