Как изменить / создать собственный FileProvider в.NET Core, который зависит от домена (то есть одно веб-приложение, обслуживающее несколько логик рендеринга сайта)
В настоящее время я создаю мультитенантное веб-приложение с использованием.NET Core. И сталкивается с проблемой:
1) Веб-приложение предоставляет различные представления и логику на основе набора доменных имен.
2) Представления являются представлениями MVC и хранятся в хранилище BLOB-объектов Azure.
3) Множественные сайты используют одни и те же контроллеры.NET Core MVC, поэтому только представления Razor отличаются небольшими логическими схемами.
Вопросы.... А) Возможно ли это? Я создал MiddleWare для манипулирования, однако я не мог назначить FileProviders на уровне контекста должным образом, потому что поставщик файлов должен зависеть от домена.
Б) Или вместо того, чтобы думать и пытаться использовать FileProvider, есть ли другой способ достичь того, чего я хочу достичь?
Большое спасибо!!!
1 ответ
Задача, которую вы описали, не совсем проста. Основная проблема здесь не в том, чтобы получить ток HttpContext
, это может быть легко сделано с IHttpContextAccessor
, Главное препятствие, с которым вы столкнетесь, заключается в том, что Razor View Engine интенсивно использует кэши.
Плохая новость заключается в том, что запрашиваемое доменное имя не является частью ключа в этих кешах, только ключ просмотра относится к подпути. Так что если вы запрашиваете представление с подпути /Views/Home/Index.cshtml
для domain1 он будет загружен, скомпилирован и кэширован. Затем вы запрашиваете представление с тем же путем, но внутри домена2. Вы ожидаете получить другое представление, специфичное для domain2, но Razor это не волнует, оно даже не вызовет ваш кастом FileProvider
, поскольку будет использоваться кэшированное представление.
Обычно Razor использует 2 кэша:
Первый ViewLookupCache
в RazorViewEngine объявлен как:
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
который разделяет данные кеша для разных доменов. Теперь с 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);
}
}
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
это только для образца. Вы должны поместить свою реализацию на основе хранилища BLOB-объектов Azure. Вы можете получить доменное имя текущего запроса, позвонив MultiTenantHelper.CurrentRequestDomain
, Вы должны быть готовы к тому, что GetFileInfo()
метод будет вызван во время запуска приложения из app.UseMvc()
вызов. Это происходит для /Pages/_ViewImports.cshtml
а также /_ViewImports.cshtml
файлы, которые импортируют пространства имен, используемые всеми другими представлениями. поскольку GetFileInfo()
вызывается не в любом запросе, IHttpContextAccessor.HttpContext
вернусь null
, Так что вы должны иметь собственную копию _ViewImports.cshtml
для каждого домена и для этих начальных вызовов возврат IFileInfo
с Exists
установлен в false
, Или сохранить PhysicalFileProvider
в бритве 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);
});
}
В 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?}");
});
}
Вы можете проверить 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());
});