ASP.NET MVC 6: view components in a separate assembly

I have done some researching on Github and found that PhysicalFileProvider (link) IFileInfo GetFileInfo(string subpath) method is used by Razor engine (link) for getting real files to compile.

Current implementation of this method

public IFileInfo GetFileInfo(string subpath)
{
     if (string.IsNullOrEmpty(subpath))
     {
         return new NotFoundFileInfo(subpath);
     }

     // Relative paths starting with a leading slash okay
     if (subpath.StartsWith("/", StringComparison.Ordinal))
     {
         subpath = subpath.Substring(1);
     }

     // Absolute paths not permitted.
     if (Path.IsPathRooted(subpath))
     {
         return new NotFoundFileInfo(subpath);
     }

     var fullPath = GetFullPath(subpath);
     if (fullPath == null)
     {
         return new NotFoundFileInfo(subpath);
     }

     var fileInfo = new FileInfo(fullPath);
     if (FileSystemInfoHelper.IsHiddenFile(fileInfo))
     {
         return new NotFoundFileInfo(subpath);
     }

     return new PhysicalFileInfo(_filesWatcher, fileInfo);
}

private string GetFullPath(string path)
{
    var fullPath = Path.GetFullPath(Path.Combine(Root, path));
    if (!IsUnderneathRoot(fullPath))
    {
        return null;
    }
    return fullPath;
}

We can see here that absolute paths nor permitted and the GetFullPath method combines path with Root which is your main web application root path.

So I assume that u can't open ViewComponent from other folder than the current one.


Update 2017-03-09

Things have changed a bit in Visual Studio 2017 using MS Build. Luckily it's much simpler. Here's how to get this to work:

In the external assembly, add this to the csproj file:

<ItemGroup>
   <EmbeddedResource Include="Views/**/*.cshtml" />
</ItemGroup>

In the main web project, add this NuGet package: Microsoft.Extensions.FileProviders.Embedded

Then in Startup, add the external assembly to the list of File Providers:

    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.FileProviders.Add(new EmbeddedFileProvider(
             typeof(SampleClassInAssembly).Assembly
             # Prior to .Net Standard 2.0
             # typeof(SampleClassInAssembly).GetTypeInfo().Assembly
        ));
    });

I'll leave the original answer below for now, in case people are still trying to get this to work with older versions of .Net Core and project.json.

================================================================

Here are the steps to make this work.

  • Make sure your view structure in the components assembly is the same as your web project. Note that there was a mistake in the screenshot that I posted along with my question.
  • Register CompositeFileProvider in Startup.cs of the web project:

    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.FileProvider = new CompositeFileProvider(
            new EmbeddedFileProvider(
                typeof(BookOfTheMonthViewComponent).GetTypeInfo().Assembly,
                "BookStore.Components"
            ),
            options.FileProvider
        );
    });
    

Both CompositeFileProvider and EmbeddedFileProvider are new, so you'll need to get these from the aspnetvnext NuGet feed. I did this by adding this source:

enter image description here

Add the dependencies in project.json:

"Microsoft.AspNet.FileProviders.Composite": "1.0.0-*",
"Microsoft.AspNet.FileProviders.Embedded": "1.0.0-*",

Lastly, add this to the project.json of the Components assembly:

"resource": "Views/**"

That should be enough to get this working.

Here is a working demo: https://github.com/johnnyoshika/mvc6-view-components/tree/master

This answer was formulated from this discussion here: https://github.com/aspnet/Mvc/issues/3750

Update 2016-01-15 There is currently one painful problem with external view components. Any changes you make to the view cshtml file does not automatically get recompiled. Even a forced Visual Studio clean and rebuild doesn't do it. You need to change a .cs file in the components assembly in order to trigger a view recompilation, but it looks like this is something that will be corrected in the future. The reason for this problem is explained here: https://github.com/aspnet/Mvc/issues/3750#issuecomment-171765303