My environment: ASP.NET Core 5 with RazorPages, Webpack 5.
In razor pages (.cshtml
) that reference svg files, I want to inline them. This is something Webpack can do (via a plugin), but I'm not sure how to integrate these two tech stacks.
I could write templatised cshtml files, and populate them via webpack:
ContactUs.cshtml.cs
ContactUs.cshtml <------ read by webpack
ContactUs.generated.cshtml <------ generated by webpack
But then how do I force msbuild / aspnet to use the generated file (ContactUs.generated.cshtml
) instead of the template file (ContactUs.cshtml
) when building?
I suspect the answer is to use IPageRouteModelConvention
but I'm unsure how.
(A dirty workaround is to instead use the filenames ContactUs.template.cshtml
and ContactUs.cshtml
but I prefer something like the above, as "generated" is clearer.)
To simplify the problem:
The compiler looks for Foo.cshtml.cs
and Foo.cshtml
.
How do I tell it to instead look for Foo.cshtml.cs
and Foo.generated.cshtml
?
When loading the app, the framework loads for you a set of PageRouteModel
s which is auto-generated from the razor page folders (by convention). Each such model contains a set of SelectorModel
each one of which has an AttributeRouteModel
. What you need to do is just modify that AttributeRouteModel.Template
by removing the suffixed part from the auto-generated value.
You can create a custom IPageRouteModelConvention
to target each PageRouteModel
. However that way you cannot ensure the routes from being duplicated (because after modifying the AttributeRouteModel.Template
, it may become duplicate with some other existing route). Unless you have to manage a shared set of route templates. Instead you can create a custom IPageRouteModelProvider
. It provides all the PageRouteModel
s in one place so that you can modify & add or remove any. This way it's so convenient that you can support 2 razor pages in which one page is more prioritized over the other (e.g: you have Index.cshtml
and Index.generated.cshtml
and you want it to pick Index.generated.cshtml
. If that generated view is not existed, the default Index.cshtml
will be used).
So here is the detailed code:
public class SuffixedNamePageRouteModelProvider : IPageRouteModelProvider
{
public SuffixedNamePageRouteModelProvider(string pageNameSuffix, int order = 0)
{
_pageNameSuffixPattern = string.IsNullOrEmpty(pageNameSuffix) ? "" : $"\\.{Regex.Escape(pageNameSuffix)}$";
Order = order;
}
readonly string _pageNameSuffixPattern;
public int Order { get; }
public void OnProvidersExecuted(PageRouteModelProviderContext context)
{
}
public void OnProvidersExecuting(PageRouteModelProviderContext context)
{
if(_pageNameSuffixPattern == "") return;
var suffixedRoutes = context.RouteModels.Where(e => Regex.IsMatch(e.ViewEnginePath, _pageNameSuffixPattern)).ToList();
var overriddenRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var route in suffixedRoutes)
{
//NOTE: this is not required to help it pick the right page we want.
//But it's necessary for other related code to work properly (e.g: link generation, ...)
//we need to update the "page" route data as well
route.RouteValues["page"] = Regex.Replace(route.RouteValues["page"], _pageNameSuffixPattern, "");
var overriddenRoute = Regex.Replace(route.ViewEnginePath, _pageNameSuffixPattern, "");
var isIndexRoute = overriddenRoute.EndsWith("/index", StringComparison.OrdinalIgnoreCase);
foreach (var selector in route.Selectors.Where(e => e.AttributeRouteModel?.Template != null))
{
var template = Regex.Replace(selector.AttributeRouteModel.Template, _pageNameSuffixPattern, "");
if (template != selector.AttributeRouteModel.Template)
{
selector.AttributeRouteModel.Template = template;
overriddenRoutes.Add($"/{template.TrimStart('/')}");
selector.AttributeRouteModel.SuppressLinkGeneration = isIndexRoute;
}
}
//Add another selector for routing to the same page from another path.
//Here we add the root path to select the index page
if (isIndexRoute)
{
var defaultTemplate = Regex.Replace(overriddenRoute, "/index$", "", RegexOptions.IgnoreCase);
route.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel() { Template = defaultTemplate }
});
}
}
//remove the overridden routes to avoid exception of duplicate routes
foreach (var route in context.RouteModels.Where(e => overriddenRoutes.Contains(e.ViewEnginePath)).ToList())
{
context.RouteModels.Remove(route);
}
}
}
Register the IPageRouteModelProvider
in Startup.ConfigureServices
:
services.AddSingleton<IPageRouteModelProvider>(new SuffixedNamePageRouteModelProvider("generated"));