I'm trying to get view components to load from a separate assembly. I posted a question on how to do this on StackOverflow and it doesn't seem like anyone knows how to do this. Any chance someone can weigh in on the subject here?
http://stackoverflow.com/q/34236850/188740
Thanks!
RazorViewEngineOptions.FileProvider with a CompositeFileProviderC#
services.Configure<RazorViewEngineOptions>(options =>
{
var embeddedFileProvider = new EmbeddedFileProvider( typeof(BookOfTheMonkViewComponent).GetTypeInfo().Assembly);
var compositeFileProvider = new CompositeFileProvider(
embeddedFileProvider,
options.FileProvider);
options.FileProvider = compositeFileProvider;
}
I haven't actually tested this out, so let me know if it fails in some awful way.
@pranavkm where CompositeFileProvider is defined ? Does it included in RC1?
I can't find CompositeFileProvider either. There seems to be a test implementation example of IFileProvider here, but no real CompositeFileProvider class:
https://github.com/aspnet/Mvc/blob/dev/test/Microsoft.AspNet.Mvc.TestCommon/TestFileProvider.cs
The changes went in farely recently so you'd have to point to the aspnetvnext feeds to pick up these packages. https://github.com/aspnet/FileSystem/tree/dev/src/Microsoft.AspNet.FileProviders.Composite
Thanks you @pranavkm!
CompositeFileProvider requires a collection of IFileProviders. In your example, you're passing an assembly to the CompositeFileProvider constructor. How can I get an IFileProvider from the assembly so that I can pass it to CompositeFileProvider?
Thanks,
Johnny
My bad - corrected my code snippet. It should be
var embeddedFileProvider = new EmbeddedFileProvider( typeof(BookOfTheMonkViewComponent).GetTypeInfo().Assembly);
Thank you @pranavkm. Everything compiles now, but unfortunately it still doesn't work. Here's the response I get on a page that contains a view component from another assembly:
HTTP/1.1 500 Internal Server Error
Content-Length: 0
Server: Kestrel
X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcSm9obm55XERvY3VtZW50c1xHaXRIdWJcZGVtb1xEZW1vLk1WQzZcd3d3cm9vdFxob21lXGxpYnJhcnk=?=
X-Powered-By: ASP.NET
Date: Thu, 17 Dec 2015 23:11:55 GMT
When I copy the cshtml into the proper directory in my startup web project, then it works, so the view component .cs file is registered properly, but not the cshtml file.
Can you turn on the developer exception page and include the exception stack trace?
@pranavkm: I think this is what you're looking for?
System.InvalidOperationException occurred
HResult=-2146233079
Message=The view 'Components/BookOfTheMonth/Default' was not found. The following locations were searched:
/Views/Home/Components/BookOfTheMonth/Default.cshtml
/Views/Shared/Components/BookOfTheMonth/Default.cshtml.
Source=Microsoft.AspNet.Mvc.ViewFeatures
StackTrace:
at Microsoft.AspNet.Mvc.ViewEngines.ViewEngineResult.EnsureSuccessful()
at Microsoft.AspNet.Mvc.ViewComponents.ViewViewComponentResult.FindView(ActionContext context, IViewEngine viewEngine, String viewName)
at Microsoft.AspNet.Mvc.ViewComponents.ViewViewComponentResult.<ExecuteAsync>d__20.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNet.Mvc.ViewComponents.ViewViewComponentResult.Execute(ViewComponentContext context)
at Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentInvoker.Invoke(ViewComponentContext context)
at Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentHelper.InvokeCore(TextWriter writer, ViewComponentDescriptor descriptor, Object[] arguments)
at Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentHelper.Invoke(String name, Object[] arguments)
at Asp.ASPV__Views_Home_Library_cshtml.<ExecuteAsync>d__17.MoveNext() in /Views/Home/Library.cshtml:line 9
InnerException:
Here's a screenshot as well in case it's useful:

Here's my code in Startup.cs
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProvider = new CompositeFileProvider(
new EmbeddedFileProvider(
typeof(BookOfTheMonthViewComponent).GetTypeInfo().Assembly
),
options.FileProvider
);
});
Going by your image in the StackOverflow post, you would need to move the Shared directory to a directory named Views. (look at the paths it's looking for). In addition, you would need to specify a base namespace for the EmbeddedFileSystem. See https://github.com/aspnet/Mvc/blob/dev/samples/EmbeddedViewSample.Web/Startup.cs#L25. For instance in your case, the namespace would be BookStore.Components
@pranavkm: thanks for noticing the error in my folder structure. I made the change and I added baseNamespace but I still get the same error.
System.InvalidOperationException occurred
HResult=-2146233079
Message=The view 'Components/BookOfTheMonth/Default' was not found. The following locations were searched:
/Views/Home/Components/BookOfTheMonth/Default.cshtml
/Views/Shared/Components/BookOfTheMonth/Default.cshtml.
Source=Microsoft.AspNet.Mvc.ViewFeatures
StackTrace:
at Microsoft.AspNet.Mvc.ViewEngines.ViewEngineResult.EnsureSuccessful()
at Microsoft.AspNet.Mvc.ViewComponents.ViewViewComponentResult.FindView(ActionContext context, IViewEngine viewEngine, String viewName)
at Microsoft.AspNet.Mvc.ViewComponents.ViewViewComponentResult.<ExecuteAsync>d__20.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNet.Mvc.ViewComponents.ViewViewComponentResult.Execute(ViewComponentContext context)
at Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentInvoker.Invoke(ViewComponentContext context)
at Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentHelper.InvokeCore(TextWriter writer, ViewComponentDescriptor descriptor, Object[] arguments)
at Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentHelper.Invoke(String name, Object[] arguments)
at Asp.ASPV__Views_Home_Library_cshtml.<ExecuteAsync>d__17.MoveNext() in /Views/Home/Library.cshtml:line 9
InnerException:
Here's my updated fileprovider registration:
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProvider = new CompositeFileProvider(
new EmbeddedFileProvider(
typeof(BookOfTheMonthViewComponent).GetTypeInfo().Assembly,
"BookStore.Components"
),
options.FileProvider
);
});
Updated folder structure:

I also tried renaming the Shared folder to Home and it still didn't work.
Are you embedding your views as resources? https://github.com/aspnet/Mvc/blob/dev/samples/EmbeddedViewSample.Web/project.json#L6
Wow, this must be my missing piece. Would I add this to my startup web project's project.json or the project where the component is defined? I've tried a few combinations but they haven't worked yet.
I guess you should add in the class library's project.json
Added this to class library's project.json but no luck. :-(
"resource": "Views/**"
I take back what I said. I created a new solution with only the absolute minimum parts, and it works. I shared this solution on GitHub:
https://github.com/johnnyoshika/mvc6-view-components
Now I have to figure out why this technique doesn't work in my other project. :-(
Very cool!
I assume this should work just fine for page views as well as components right? I'm trying to automatically add file providers from referenced assemblies that contain views.
In my Startup.cs -
public void ConfigureServices(IServiceCollection services) {
var libNames = _libraryManager.GetLibraries().Select(lib => lib.Name).Distinct();
var assemblies = new List<Assembly>();
var fileProviders = new List<IFileProvider>();
foreach (var libName in libNames.Where(libName => libName.StartsWith("Module")))
{
Assembly assm;
try
{
assm = Assembly.Load(new AssemblyName(libName));
}
catch
{
continue;
}
assemblies.Add(assm);
fileProviders.Add(new EmbeddedFileProvider(assm));
}
services.AddMvc()
.AddPrecompiledRazorViews(assemblies.ToArray());
services.Configure<RazorViewEngineOptions>(options =>
{
fileProviders.Add(options.FileProvider);
options.FileProvider = new CompositeFileProvider(fileProviders.ToArray());
});
}
I realize I'm not passing in the baseNamespace parameter to EmbeddedFileProvider(). I've tried it both ways, but it hasn't changed the behavior.
Here is my class library package containing the view and controller I want to reference.

This is what the package .dll contains for my embedded view resource:

So my controller is attempting to explicitly call the view by this resource name:

However, still no luck in resolving the view:

My wwwroot project dependencies:
"dependencies": {
"Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
"Microsoft.AspNet.FileProviders.Composite": "1.0.0-rc2-*",
"Microsoft.AspNet.FileProviders.Embedded": "1.0.0-rc2-*",
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
"Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
"Microsoft.Extensions.PlatformAbstractions": "1.0.0-rc1-final",
"Module.StatusMonitor": "1.0.0-*",
"System.Reflection": "4.1.0-beta-23516"
}
... and my module package library dependencies:
"dependencies": {
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.CSharp": "4.0.1-rc2-*",
"System.Collections": "4.0.11-rc2-*",
"System.Linq": "4.0.1-rc2-*",
"System.Runtime": "4.0.21-rc2-*",
"System.Threading": "4.0.11-rc2-*"
}
Any thoughts or suggestions are much appreciated!
@Morgma: I got page views from separate assemblies to load without any problem using the same technique as view components. I describe it here:
http://stackoverflow.com/a/34366119/188740
And here's a working example:
@johnnyoshika I had been working off your solution previously, but now revisiting it, I have found my issue. The baseNamespace is definitely required to make this work. Additionally, my approach to using ILibraryManager to find the assemblies doesn't work. My guess is that even though I reference the module in project.json and the assembly is found in library manager, the compiler never loads the assembly unless there is a reference to the namespace in a "using". I was hoping to get to a point where I could install the module NuGet and automatically handle everything I needed to essentially load a new "Area" within the main web project.
Finally, for some reason, Visual Studio is working against me by intellisense declaring the usings unnecessary, though the solution builds fine despite the red and ultimately does work. This is even in the case of a fresh clone of your solution:

Thanks for your code; helped me through these gotchas.
Now that I've determined my root issue, though it was the first thing I discounted in my initial post, is that the defaultNamespace parameter was not set in my EmbeddedFileProvider. I was able to return to dynamically loading the assemblies based on the convention of starting with "Module" and all is working well. Thanks again for your help @johnnyoshika !
Here's an example of how I'm loading in all of my View providers:
``` C#
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var libNames = _libraryManager.GetLibraries().Select(lib => lib.Name).Distinct();
var assemblies = new List<Assembly>();
var fileProviders = new List<IFileProvider>();
foreach (var libName in libNames.Where(libName => libName.StartsWith("Module")))
{
Assembly assm;
try
{
assm = Assembly.Load(new AssemblyName(libName));
}
catch
{
continue;
}
assemblies.Add(assm);
var defaultNamespace = assm.FullName.Split(',')[0];
fileProviders.Add(new EmbeddedFileProvider(assm, defaultNamespace));
}
services.Configure<RazorViewEngineOptions>(options =>
{
fileProviders.Add(options.FileProvider);
options.FileProvider = new CompositeFileProvider(fileProviders.ToArray());
});
}
```
@Morgma: fantastic!
Great solution, exactly what I was looking for.
What about things other than views though. How would you access things such as js, css or image files in the wwwroot of the external assembly?
The StaticFileMiddleware allows specifying a file provider. Perhaps specifying the CompositeFileProvider as it's file system view would suffice?
@pranavkm In my case, the external assemblies are not standalone web projects with a wwwroot; just NuGet package class libraries. Within the same project source, I'm exposing a Bower package that will supply the static files. My core web app will use Gulp to provision these dependencies to the right locations and do any transformations (script ordering, namespacing, etc...)
I'm still working through some implementation details, but seems to be a viable approach to getting to the level of isolation I need between modules.
The idea would be to embed resources in the class library and register a static file middleware with a composite file system - similar to the way the composite file system for views is created.,
Thanks for the advice. I had tried the following in the Configure method to use a composite file provider for static files in
c#
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new CompositeFileProvider(fileProviders.ToArray()),
});
But Im not sure how to then access the static files from client side code, whatever I try just results in a 404 error. Im not sure if im embeddingthe files as resources properly in my project.json or if im adding the file properly in my html.
I will give @Morgma approach a try but I havent used Gulp before and was trying to get around having to write a load of scripts to pull the files in as this would mean possibly having to update those scripts when changes are made to the external assembly.
I was hoping there would be a way to do this without using scripts to bring the files in
@pranavkm: I noticed a big problem with having view components in a separate assembly. Changes to Views are not picked up and doesn't get recompiled. I tried cleaning and rebuilding, but it didn't help. The only way to pick up the changes were to change a .cs file inside the view components assembly, which forced a recompilation. Is there any way around this?
@johnnyoshika, the issue is related to DNX's behavior - https://github.com/aspnet/Mvc/issues/3244#issuecomment-146016999.
We're in the process of migrating from DNX to dotnet-cli for compilation and building. At this point the CLI doesn't have incremental build semantics, so it always rebuilds all dependencies which should resolve your issue. I'm not sure what their behavior would be when they add incremental compilation (https://github.com/dotnet/cli/issues/838 tracks the change), perhaps they are better about it.
For now, besides editing the cs file, I'm not sure there is a workaround.
Thanks for the information, @pranavkm.
I am hitting an issue where external View components only appear to work when there is an assembly / project reference to the VC from the website project.
If you are just dropping an assembly containing the VC (And it's embedded razor view) into some folder, and the website is dynamically discovering the assembly when starting up, then it stops working. I'll put together a repro on github to demonstrate.
Repro here: https://github.com/dazinator/mvc6-view-components/tree/issue-noprojectreferences
I simply forked @johnnyoshika working example, removed the project references and loaded the assemblies (for the external VC, and for the external Controller) dynamically instead.
The external controller still works, the VC doesn't..
@dazinator: That's interesting. I wonder why your technique doesn't work.
I've been pulling my hair over this for over a week now and I cannot get it to work. I have a web project Vikekh.Web in which I am adding one EmbeddedFileProvider, supplying the constructor with the assembly derived from my module's controller and the default namespace.
var fileProviders = new List<IFileProvider>();
var moduleAssembly = typeof(Vikekh.Modules.ModuleA.Controllers.ModuleAController).GetTypeInfo().Assembly;
fileProviders.Add(new EmbeddedFileProvider(
moduleAssembly,
moduleAssembly.FullName.Split(',')[0]
));
services.Configure<RazorViewEngineOptions>(o =>
{
o.FileProvider = new CompositeFileProvider(fileProviders);
});
The module project has its views in Vikekh.Modules.ModuleA\Areas\ModuleA\Views\ModuleA and I have specified the following in the project.config:
"resource": [ "Areas/ModuleA/Views/**", "Areas/ModuleA/Content/**" ]
But when I try to access I get a 500 Internal Server Error and:
InvalidOperationException: The view 'Foo' was not found. The following locations were searched:
/Views/ModuleA/Foo.cshtml
/Views/Shared/Foo.cshtml.
The controller action is hit. Any ideas? I am on *-rc1-final, but had to pull in Microsoft.AspNet.FileProviders.Composite at rc2 from the aspnetvnext feed.
@vikekh
You might want to use the default file provider, in addition to your own. At the moment I think you are overwriting the default file provider.
var embeddedFileProviders = componentAssemblies.Select(a => (IFileProvider)new EmbeddedFileProvider(a, a.GetName().Name)).ToList();
services.Configure<RazorViewEngineOptions>(options =>
{
embeddedFileProviders.Add(options.FileProvider);
options.FileProvider = new CompositeFileProvider(embeddedFileProviders);
});
Also you are using areas, did you set up a route?
@vikekh does your controller need to be marked with an AreaAttribute? Your project.json includes views from the Areas/ModuleA/Views, but the view engine is looking for non-area views.
@johnnyoshika
That's interesting. I wonder why your technique doesn't work.
I have no idea why it doesn't work haha. I was hoping someone can shed some light on it or perhaps there is something I was missing. I have a feeling its a genuine MVC 6 issue though.
The only thing I can think of is that it't an oversight, in that there is a method for registering controllers in stand alone assemblies, but nothing for doing the same for VC's.
I.e there is no equivalent to the following, for VC's
services.AddMvc().AddControllersAsServices(new[] { assembly });
If that's correct, I'm assuming there must be some other class you need to implement to provide the candidate assemblies for VC discovery, but I am not sure what that is!
@dazinator I added the default file provider, however it always seems to be null.
@pranavkm Having an area attribute makes the controller method unreachable.
There may be other custom implementations in the web project since I'm not the one who set it up. However, I am responsible for upgrading from Beta 7 to RC1. Previously the path Vikekh.Modules.ModuleA\Areas\ModuleA was added as a PhysicalFileProvider but this doesn't seem to work anymore--therefor I am attempting this solution.
@vikekh about the null file provider.. that definately shouldn't be the case and i also hit that samr problem when I was using autofac. The solution for me was:
Something you can try is to move your services.ConfigureOptions
call above services.AddMvc() to see if it makes a difference.
@dazinator Already is above!
@vikekh
:-(
I.e there is no equivalent to the following, for VC's
services.AddMvc().AddControllersAsServices(new[] { assembly });
@vikekh You would have to register a StaticAssemblyProvider to do this. It's a bit tangential to this issue, could you start a new issue for it?
@pranavkm - it was actually me who mentioned that! I have just raised it as a seperate issue as requested #4087
Any updates on this? There have been some changes to the framework and the solution presented does no longer work. Please provide guidance on adding external views/controllers and view components.
@genbox MVC 6 rtm has a new "application parts" API. All you need to do is register your assembly containg your viewcomponent with that Api. If you assembly is allready referenced then you shouldnt need to bother, this is just if you load assemblies at runtime.
@dazinator I'm loading assemblies at runtime. Could you point me in the direction of this API? A Google search for application parts was unfruitful.
@genbox from memory you can do something like:
Assembly someassembly = GetAssembly();
services.AddMvc().AddApplicationPart(assembly);
So in other words the Api is on the builder returned via AddMvc();
This seem to work fine with controllers, but what about views?
Edit: Searched a bit for AddApplicationPart() and this article came up: http://www.codeproject.com/Articles/1109475/WebControls/ it also addresses how the view discovery works.
I think this can be closed now, as it has been established that VC's do work in seperate assemblies..
@dazinator I just tried loading view components from another assembly with the newly released VS 2017, and it looks like the views aren't being discovered. I get the standard old error message:
InvalidOperationException: The view 'Components/CategoryDetails/Default' was not found. The following locations were searched:
/Views/Home/Components/CategoryDetails/Default.cshtml
/Views/Shared/Components/CategoryDetails/Default.cshtml
The separate assembly is being referenced. I also tried services.AddMvc().AddApplicationPart(assembly) but it still doesn't work. Without the view, the View Component loads just fine (e.g. if I return ContentViewComponentResult instead of ViewViewComponentResult). Thoughts?
Not taken the plunge and upgraded to vs2017 yet. I'm still in project.json land, and targeting asp.net core 1.0.
Some things you might want to check though:
fileProvider.GetFileInfo("/some/module/foo.cshtml"). The file info returned should have an Exists property and the ability to create a read stream so you can test reading the file.If you can resolve the view using your FileProvider then I'm thinking your issue is probably about getting MVC to use your file provider for view resolution. If you can't then I'm guessing you have an issue with your file provider setup.
@dazinator I got it to work. Thanks for all of the tips. Things have changed a bit with MS Build.
In the external assembly, we need to include this in csproj:
<ItemGroup>
<EmbeddedResource Include="Views/**/*.cshtml" />
</ItemGroup>
Then in the executing project, we need to reference this NuGet package: Microsoft.Extensions.FileProviders.Embedded
Then in Startup, we can do this:
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Add(new EmbeddedFileProvider(
assembly
));
});
Interestingly, changing views in external assemblies doesn't get reflected right away. i.e. reloading the browser doesn't reflect any changes. It requires a full compile for the changes to show up. So it looks like this problem hasn't been fully addressed yet, although it's better than it was before. Before even a compile wasn't enough...you actually had to change and save a C# file.
Are you using embedded views? I solved this problem by only using Embedded views for production builds. When running under development mode, rather than creating EmbeddedFileProvider's that wrap the assembly , I create PhsyicalFileProviders that point to the actual project source code location as the content root folder. This has the benefit that you can just edit the cshtml view that's in the separate project, and it will update in real time without needing to build. Ofcourse when you deploy your application, you won't be deploying all of the source code for these projects, and that's when you'd fall back to using embedded file providers instead.
Are you using embedded views? I solved this problem by only using Embedded views for production builds.
Sadly this is the reality for embedded views. They are baked into the assembly at compile-time, and we won't ever look on disk because we don't know where to look.
Are you using embedded views? I solved this problem by only using Embedded views for production builds. When running under development mode, rather than creating EmbeddedFileProvider's that wrap the assembly , I create PhsyicalFileProviders that point to the actual project source code location as the content root folder. This has the benefit that you can just edit the cshtml view that's in the separate project, and it will update in real time without needing to build. Ofcourse when you deploy your application, you won't be deploying all of the source code for these projects, and that's when you'd fall back to using embedded file providers instead.
Here's a sample code of @dazinator's technique:
if (Env.IsDevelopment())
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Add(new PhysicalFileProvider(Regex.Replace(Env.ContentRootPath, $"{Env.ApplicationName}$", "PortalComponents")));
});
else
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Add(new EmbeddedFileProvider(
typeof(CategoryDetails).GetTypeInfo().Assembly,
"PortalComponents"
));
});