I'd like to define view components (which are new in ASP.NET MVC 6) in a separate assembly from the MVC 6 web startup project so that I can reuse them in multiple web projects. A sample solution might look like this:
- BookStore.Components (houses common view components)
- BookStore.Web1 (references BookStore.Components)
- BookStore.Web2 (references BookStore.Components)
I created a new Class Library (Package) and created a view component inside. I also created the view following the nested folder convention. My BookStore.Components
project looks like this:
When I try to invoke this view component from my web project:
@Component.Invoke("BookOfTheMonth")
...I get a 500 error with an empty content body. It seems like the ViewComponent class is discovered, but the razor view for the component isn't.
I also tried to extend DefaultViewComponentDescriptorProvider
so that view components from the BookStore.Components
assembly can be discovered:
Defined an AssemblyProvider
public class AssemblyProvider : IAssemblyProvider
{
public IEnumerable<Assembly> CandidateAssemblies
{
get
{
yield return typeof(AssemblyProvider).Assembly;
yield return typeof(BookStore.Components.BookOfTheMonthViewComponent).Assembly;
}
}
}
Registered AssemblyProvider using Autofac
builder.RegisterType<AssemblyProvider>()
.AsImplementedInterfaces();
builder.RegisterType<DefaultViewComponentDescriptorProvider>()
.AsImplementedInterfaces();
I'm not sure if the registration of DefaultViewComponentDescriptorProvider
above is needed or not, so I tried with and without it, but I still get a 500 error on a page where the view component is invoked.
How can I invoke a view component that lives in a separate assembly from the MVC6 web project?
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:
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
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.