I'm currently creating a multi-tenant web app using .NET Core. And is facing a problem:
1) The Web App serves different views and logics based on a set of domain names.
2) The views are MVC views and stored in Azure Blob Storage
3) The multiples sites share the same .NET Core MVC controllers therefore only the Razor views are different with small logics.
Questions.... A) Is that possible? I created a MiddleWare to manipulate however I couldn't assign FileProviders on context level properly because the file provider should be domain dependant.
B) Or, rather than thinking and attempting through FileProvider, is there any other way to achieve what I want to achieve?
Many thanks!!!
The task you described is not quite simple. The main problem here is not to get current
HttpContext
, it could be easily done withIHttpContextAccessor
. The main obstacle you will face is that Razor View Engine makes heavy use of the caches.The bad news are that request domain name is not a part of the key in those caches, only view subpath belongs to a key. So if you request a view with subpath
/Views/Home/Index.cshtml
for domain1, it will be loaded, compiled and cached. Then you request a view with the same path but within domain2. You expect to get another view, specific for domain2, but Razor does not care, it will not even call your customFileProvider
, since the cached view will be used.There are basically 2 caches used by Razor:
First one is
ViewLookupCache
in RazorViewEngine declared as:Well, the things are getting worse. This property is declared as non-virtual and does not have a setter. So it's not quite easy to extend
RazorViewEngine
with view cache that has a domain as part of the key.RazorViewEngine
is registered as singleton and is injected intoPageResultExecutor
class which is also registered as singleton. So we don't have a way of resolving new instance ofRazorViewEngine
for each domain, so that it has its own cache. Seems like the easiest workaround for this problem is to set the propertyViewLookupCache
(despite the fact that it does not have a setter) to the multi-tenant implementation ofIMemoryCache
. Setting the property without a setter is possible however it's a very dirty hack. At the moment I propose such workaround to you, God kills a kitten. However I don't see a better option to bypassRazorViewEngine
, it just is not flexible enough for this scenario.The second Razor cache is
_precompiledViewLookup
in RazorViewCompiler:This cache is stored as private field, however we could have new instance of
RazorViewCompiler
for each domain, since it's intantiated byIViewCompilerProvider
which we could implement in multi-tenant way.So keeping all this in mind, let's do the job.
MultiTenantRazorViewEngine class
MultiTenantRazorViewEngine
derives fromRazorViewEngine
and setsViewLookupCache
property to instance ofMultiTenantMemoryCache
.MultiTenantMemoryCache class
MultiTenantMemoryCache
is an implementation ofIMemoryCache
that separates cache data for different domains. Now withMultiTenantRazorViewEngine
andMultiTenantMemoryCache
we added domain name to the first cache layer of the Razor.MultiTenantRazorPageFactoryProvider class
MultiTenantRazorPageFactoryProvider
creates separate instance ofDefaultRazorPageFactoryProvider
so that we have a distinct instance ofRazorViewCompiler
for each domain. Now we have added domain name to the second cache layer of the Razor.MultiTenantHelper class
MultiTenantHelper
provides access to current request and domain name of this request. Unfortunately we have to declare it as static class with static accessor forIHttpContextAccessor
. Both Razor and static files middleware do not allow to set new instance ofFileProvider
for each request (see below inStartup
class). That's whyIHttpContextAccessor
is not injected intoFileProvider
and is accessed as static property.MultiTenantFileProvider class
This implementation of
MultiTenantFileProvider
is just for sample. You should put your implementation based on Azure Blob Storage. You could get domain name of current request by callingMultiTenantHelper.CurrentRequestDomain
. You should be ready thatGetFileInfo()
method will be called during application startup fromapp.UseMvc()
call. It happens for/Pages/_ViewImports.cshtml
and/_ViewImports.cshtml
files which import namespaces used by all other views. SinceGetFileInfo()
is called not within any request,IHttpContextAccessor.HttpContext
will returnnull
. So you should either have own copy of_ViewImports.cshtml
for each domain and for these initial calls returnIFileInfo
withExists
set tofalse
. Or to keepPhysicalFileProvider
in RazorFileProviders
collection so that those files could be shared by all domains. In my sample I've used former approach.Configuration (Startup class)
In
ConfigureServices()
method we should:IRazorViewEngine
withMultiTenantRazorViewEngine
.IViewCompilerProvider
with MultiTenantRazorViewEngine.IRazorPageFactoryProvider
withMultiTenantRazorPageFactoryProvider
.FileProviders
collection and add own instance ofMultiTenantFileProvider
.In
Configure()
method we should:MultiTenantHelper.ServiceProvider
.FileProvider
for static files middleware to instance ofMultiTenantFileProvider
.Sample Project on GitHub