title: Theming RazorPages in Dotnet Core 2.1
body: <p>I am currently trying to implement a themeable site using Razor Pages in dotnetcore 2.1 but am having some trouble/confusion with why the pages won't load.</p>
  
  <p>Every request to the site causes a theme value to be set based on the accessed domain, by default the theme is "Default" which is stored in the RouteData of each request.</p>
  
  <p>I have implemented the following <code>ThemeViewLocationExpander</code></p>
  
  <pre><code>public class ThemeViewLocationExpander : IViewLocationExpander
  {
      private const string ValueKey = "Theme";
  
      public void PopulateValues(ViewLocationExpanderContext context)
      {
          if (context is null)
          {
              throw new ArgumentNullException(nameof(context));
          }
  
          context.Values[ValueKey] = (context.ActionContext.RouteData.Values["tenant"] as Tenant)?.Theme;
      }
  
      public IEnumerable&lt;string&gt; ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable&lt;string&gt; viewLocations)
      {
          if (context is null)
          {
              throw new ArgumentNullException(nameof(context));
          }
  
          if (viewLocations is null)
          {
              throw new ArgumentNullException(nameof(viewLocations));
          }
  
          context.Values.TryGetValue(ValueKey, out string theme);
  
          if (!string.IsNullOrEmpty(theme))
          {
              viewLocations = new[] {
                  $"/Pages/Themes/{theme}/{{1}}/{{0}}.cshtml",
                  $"/Pages/Themes/{theme}/Shared/{{0}}.cshtml",
                  $"/Pages/Shared/{{0}}.cshtml",
              };
          }
  
          return viewLocations;
      }
  </code></pre>
  
  <p>As you can see I am trying to load the pages (as each theme may have a completely different layout and not just varying CSS) from <code>/Pages/Themes/{THEME_NAME}</code>... instead of <code>/Pages/</code>, with the exception of a top level shared folder <code>/Pages/Shared</code> for things such as partials for validation scripts which may exist in all themes.</p>
  
  <p>I don't want there to be any other pages directly under <code>/Pages</code> such as <code>/Pages/Index.cshtml</code> as EVERY request should end up inside one of the theme folders as having a theme is mandatory.</p>
  
  <p>I have added the following to my <code>Startup.cs</code> </p>
  
  <pre><code>        services.Configure&lt;RazorViewEngineOptions&gt;(options =&gt;
          {
              options.ViewLocationExpanders.Add(new ThemeViewLocationExpander());
          });
  </code></pre>
  
  <p>I can breakpoint this code on first load, so I know it is registered, however if I run the site and got to <code>https://localhost:44352/</code> it loads <code>/Pages/Index.cshtml</code> when it should be loading <code>/Pages/Themes/Default/Index.cshtml</code>.</p>
  
  <p>If I delete <code>/Pages/Index.cshtml</code> and run the site I get </p>
  
  <pre><code>This localhost page can’t be found
  No webpage was found for the web address: https://localhost:44352/
  HTTP ERROR 404
  </code></pre>
  
  <p>Can anyone offer me any insight or help with this? </p>
  
  <p>Perhaps I need to modify some routing to incorporate the theme, or am I missing something major, it seems as if the view locations I set in the <code>ThemeViewLocationExpander.ExpandViewLocations()</code> never get used.</p>
  
  <p>There is a lot of multi-tenancy stuff that I have read about for DNC 2.1 but it is either for MVC not RazorPages or only implements CSS / _Layout.cshtml pages inside the theme folders and uses the default pages under <code>/Pages/</code> for layout (I am trying to have individual implementations of all pages inside each theme folder)</p>
  
  <p><strong>Edit:</strong></p>
  
  <p>I have decided it would make more sense to keep the <code>/Pages/Index.cshtml</code> and other pages in the top level folder and use that as the default 'Theme' any new themes would then use these pages unless specifically overridden inside of their <code>/Pages/Themes/{THEME_NAME}</code> folder.</p>
  
  <p>I Have implemented this, but am still at a loss as to why the page loads from <code>/Pages/Index.cshtml</code> even when a theme value is set and the <code>viewLocations</code> have the theme folder added to them.</p>
  
  <p><strong>Edit 2:</strong></p>
  
  <p>Have tested it and with a theme set it definitely used the correct layout page in the theme folder. For example, with the theme 'Fresh' when I load <code>https://localhost:44352/</code> the layout page <code>/Pages/Themes/Fresh/Shared/_Layout.cshtml</code> is loaded, however the <code>Index.cshtml</code> is loaded from <code>/Pages/Index.cshtml</code> not <code>/Pages/Themes/Fresh/Index.cshtml</code>.</p>
  
  <p>At this point the <code>viewLocations</code> were:</p>
  
  <pre><code>/Pages/Themes/Fresh/{1}/{0}.cshtml
  /Pages/Themes/Fresh/Shared/{0}.cshtml
  /Pages/Shared/{0}.cshtml
  /Pages/{1}/{0}.cshtml
  /Pages/Shared/{0}.cshtml
  /Views/Shared/{0}.cshtml
  </code></pre>
  
  <p><strong>Edit 3:</strong></p>
  
  <p>Here is a github link for a simplified version of the project that demonstrates the issue I am having. <a href="https://github.com/BenMaxfield/ThemeTest" rel="noreferrer">https://github.com/BenMaxfield/ThemeTest</a></p>
  
  <p>In order to test it, just run the application and you should see that it loads the DARK theme layout, but the DEFAULT Index content. </p>
  
  <p>To swap to the DEFAULT theme look for the <code>PopulateValues</code> method in <code>ThemePageViewLocationExpander</code> and set <code>context.Values[ThemeKey] = string.Empty;</code></p>
  
  <p><strong>Edit 4</strong></p>
  
  <p>I have noticed that <code>ThemePageViewLocationExpander.ExpandViewLocations</code> is only every called for partial files, and never for actual pages with code behind.</p>
  
  <p>This explains why the correct <code>_Layout.cshtml</code> page was loaded for the theme but the correct <code>Index.cshtml</code> file was not (as <code>/Pages/Index.cshtml</code> is a page with code behind and has the <code>@page</code> declaration - i.e it is not partial)</p>
  
  <p>To get around this in a hacky way I have created a new folder <code>/Pages/Views/</code> and put a partial <code>.cshtml</code> that corresponds to each of the <code>@page</code> pages at the top level of <code>/Pages/</code>, </p>
  
  <p>e.g I now have <code>/Pages/Index.cshtml</code> (non-partial) and <code>/Pages/Views/Index.cshtml</code> (partial).</p>
  
  <p>Inside <code>/Pages/Index.cshtml</code> I have added <code>&lt;partial name="Index" model="@Model"/&gt;</code> to load the corresponding partial from the new <code>/Pages/Views/</code> folder.</p>
  
  <p>As this is now loading a partial <code>ThemePageViewLocationExpander.ExpandViewLocations</code> is invoked and I can then override the view location for it based on the given theme key (if present).</p>
  
  <p>This means all requests to <code>/Index</code> will load <code>OnGet()</code> from <code>/Pages/Index.cshtml</code> which subsequently loads a partial in its view which I can optionally override by adding another <code>Index.cshtml</code> partial inside of <code>/Themes/{THEME_NAME}/Views/</code>.</p>
  
  <p>Here is my current viewlocation override for <code>ThemePageViewLocationExpander.ExpandViewLocations</code></p>
  
  <pre><code>            viewLocations = new[] {
                  $"/Themes/{theme}/{{0}}.cshtml",
                  $"/Themes/{theme}/Views/{{0}}.cshtml",
                  $"/Pages/Views/{{0}}.cshtml",
              }.Concat(viewLocations);
  </code></pre>
  
  <p>And an example folder structure:</p>
  
  <pre><code>.
  ├── Pages
  |   ├── Index.cshtml (with code behind, called on localhost:45635/)
  |   └── Views
  |       └── Index.cshtml (partial)
  └── Themes
  |   ├── Dark
  |      └── Views
  |          └── Index.cshtml (partial)
  </code></pre>