ASP.NET Core MVC View Component search path

You can add additional view location formats to RazorViewEngineOptions. As an example, to add a path that satisfies your requirement, you can use something like this:

services
    .AddMvc()
    .AddRazorOptions(o =>
    {
        // /Components/{View Component Name}/{View Name}.cshtml
        o.ViewLocationFormats.Add("/{0}.cshtml");
        o.PageViewLocationFormats.Add("/{0}.cshtml");
    });

As can be seen above, there are different properties for views (when using controllers and actions) and page views (when using Razor Pages). There's also a property for areas, but I've left that out in this example to keep it marginally shorter.

The downside to this approach is that the view location formats do not apply only to view components. For example, when looking for the Index view inside of Home, Razor will now also look for Index.cshtml sitting at the root of the project. This might be fine because it's the last searched location and I expect you're not going to have any views sitting at the root of your project, but it's certainly worth being aware of.


So after an hour digging into the aspnetcore repository, I found the component's search path is hardcoded and then combined with normal view search paths.

// {0} is the component name, {1} is the view name.
private const string ViewPathFormat = "Components/{0}/{1}";

This path is then sent into the view engine

result = viewEngine.FindView(viewContext, qualifiedViewName, isMainPage: false);

The view engine then produces the full path, using the configurable view paths.

  • Views/Shared/Components/Cart/Default.cshtml
  • Views/Home/Components/Cart/Default.cshtml
  • Areas/Blog/Views/Shared/Components/Cart/Default.cshtml

If you want to place your view components into a root folder named "Components" as I wanted, you can do something like this.

services.Configure<RazorViewEngineOptions>(o =>
{
    // {2} is area, {1} is controller,{0} is the action
    // the component's path "Components/{ViewComponentName}/{ViewComponentViewName}" is in the action {0}
    o.ViewLocationFormats.Add("/{0}" + RazorViewEngine.ViewExtension);        
});

That's kind of ugly in my opinion. But it works.

You can also write your own expander like this.

namespace TestMvc
{
    using Microsoft.AspNetCore.Mvc.Razor;
    using System.Collections.Generic;

    public class ComponentViewLocationExpander : IViewLocationExpander
    {
        public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
        {
            // this also feels ugly
            // I could not find another way to detect
            // whether the view name is related to a component
            // but it's somewhat better than adding the path globally
            if (context.ViewName.StartsWith("Components"))
                return new string[] { "/{0}" + RazorViewEngine.ViewExtension };

            return viewLocations;
        }

        public void PopulateValues(ViewLocationExpanderContext context) {}
    }
}

And in Startup.cs

services.Configure<RazorViewEngineOptions>(o =>
{
    o.ViewLocationExpanders.Add(new ComponentViewLocationExpander());   
});