Search code examples
asp.net-corewebpackrazor-pagesasp.net-core-5.0

How to change the name of a .cshtml file in ASP.NET Core Razor Pages


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?


Solution

  • When loading the app, the framework loads for you a set of PageRouteModels 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 PageRouteModels 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"));