The great thing about ASP.NET has generally been its “get stuff done” attitude, with many defaults and conventions set for us right out of the box. Sane defaults mean we can be productive and build apps quickly without requiring a deep understanding of the inner-workings of ASP.NET as a development framework. The idea is excellent when everything is going well, but it can leave us frustrated when the actual results of code execution don’t match our expected results.

In this post, we’ll see how we can introspect the RazorViewEngine to see where it searches for views, helping us understand when we don’t get the results we expected.

Getting Started

This approach will work with any ASP.NET project that is using ASP.NET Core or Razor pages and is utilizing the RazorViewEngine as its main rendering view engine. If our project is using another view engine, then this approach will need to be modified.

To make sure we are using the RazorViewEngine, we need to ensure we have registered either MVC or RazorPages in our IServiceCollection.

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    // or
    services.AddMvc();
}

Note: The call to AddMvc also calls AddRazorPages internally, so we don’t need both registration calls inside our ConfigureServices method.

RazorViewEngineOptions

In the previous section, we registered many of the mechanical parts of the RazorViewEngine system. We won’t go into too much detail about all the things that were registered, but an essential type registered with our dependency injection was RazorViewEngineOptions.

RazorViewEngineOptions tells the RazorViewEngine where it should look for our views. Properties include ViewLocationExpanders, ViewLocationFormats, AreaViewLocationFormats, PageViewLocationFormats, and AreaPageViewLocationFormats.

Razor Page

We can build a Razor page to take advantage of RazorViewEngineOptions and list out the locations for each property. Note, we need to inject an instance of IOptions<RazorViewEngineOptions>, or else we’ll get an exception.

@page
@using Microsoft.Extensions.Options
@using Microsoft.AspNetCore.Mvc.Razor
@model IndexModel
@inject IOptions<RazorViewEngineOptions> Options
@{
    ViewData["Title"] = "Home page";
}

<h2>View Location Formats</h2>
<ul>
    @{
        var views =
            Options.Value.ViewLocationFormats;
    }
    @foreach (var location in views)
    {
        <li>@location</li>
    }
</ul>

<h2>Area View Location Formats</h2>
<ul>
    @{
        var areas =
            Options.Value.AreaViewLocationFormats;
    }
    @foreach (var location in areas)
    {
        <li>@location</li>
    }
</ul>

<h2>Page View Location Formats</h2>
<ul>
    @{
        var pages =
            Options.Value.PageViewLocationFormats;
    }
    @foreach (var location in pages)
    {
        <li>@location</li>
    }
</ul>

<h2>Area Page View Location Formats</h2>
<ul>
    @{
        var areaPages =
            Options.Value.AreaPageViewLocationFormats;
    }
    @foreach (var location in areaPages)
    {
        <li>@location</li>
    }
</ul>

When we run our ASP.NET application, we’ll see the following results on our rendered page.

View Location Formats
  - /Views/{1}/{0}.cshtml
  - /Views/Shared/{0}.cshtml
  - /Pages/Shared/{0}.cshtml
Area View Location Formats
  - /Areas/{2}/Views/{1}/{0}.cshtml
  - /Areas/{2}/Views/Shared/{0}.cshtml
  - /Views/Shared/{0}.cshtml
  - /Pages/Shared/{0}.cshtml
Page View Location Formats
  - /Pages/{1}/{0}.cshtml
  - /Pages/Shared/{0}.cshtml
  - /Views/Shared/{0}.cshtml
Area Page View Location Formats
  - /Areas/{2}/Pages/{1}/{0}.cshtml
  - /Areas/{2}/Pages/Shared/{0}.cshtml
  - /Areas/{2}/Views/Shared/{0}.cshtml
  - /Pages/Shared/{0}.cshtml
  - /Views/Shared/{0}.cshtml

For MVC locations, the {0} is a placeholder for the action name, where the {1} is a placeholder for the controller name, and finally any reference to {2} denotes the area’s name.

For Razor Pages locations, the {0} placeholder denotes a view’s name, where the {1} denotes a page’s name, and {2} is again the area’s name.

We can alter these locations at registration time by adding additional locations that include the placeholders required to dynamically resolve views.

Conclusion

There we have it! We can use RazorViewEngineOptions to understand where the RazorViewEngine will look for our views. We can also augment the options at registration with more locations, and this technique works great for those with a customized setup. Hope you found this post helpful, and please leave a comment below.