6

I have some modules which has controllers and views. It is basically an extension for my web application. Each module is in a class library.

I want to load these assemblies from my web application. But I'm without luck here.


My solutions file structure is like:

src
|
|-- Web.Common  (Class Library Project)
|   |- Files like: filters, my own controller etc...
|    
|-- WebApplication (ASP.NET5 WebSite)
|   |- wwwroot
|   |- Controllers
|   |- Views
|   |- etc...
|
|-- Module 1 (Class Library Project)
|   |- Controllers
|   |- Views
|
|-- Module 2 etc...

These are what I tried:


I tried to implement my own IViewLocationExpander

public class CustomViewLocationExpander : IViewLocationExpander
{
    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        yield return "/../Module1.Web/Views/Home/TestView.cshtml";
        yield return "../Module1.Web/Views/Home/TestView.cshtml";
        yield return "/Module1.Web/Views/Home/TestView.cshtml";
        yield return "~/../Module1.Web/Views/Home/TestView.cshtml";
    }

    public void PopulateValues(ViewLocationExpanderContext context)
    {

    }
}

I tried all kind of paths that came to my mind but no luck :(

I get:

InvalidOperationException: The view 'TestView' was not found. The following locations were searched:

~/Module1.Web/Views/Home/TestView.cshtml ~/../Module1.Web/Views/Home/TestView.cshtml /Module1.Web/Views/Home/TestView.cshtml /../Module1.Web/Views/Home/TestView.cshtml


So I thought maybe the default IFileProvider doesn't look outside the WebApp's root path and decided to try implementing my own IFileProvider.

But here I didn't have any success neither.


Maybe there is a feature to achieve this by calling some ASP.NET methods but I don't know it.

Any suggests?

Yves
  • 3,752
  • 4
  • 29
  • 43

3 Answers3

8

Controllers will get loaded automatically. To load views, you will need EmbeddedFileProvider and CompositeFileProvider, both of which are new, so you'll need to get them from the aspnetvnext feed.

Reference them in your startup MVC6 project's project.json:

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

Update your service registration in Startup.cs:

    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.FileProvider = new CompositeFileProvider(
            new EmbeddedFileProvider(
                typeof(BooksController).GetTypeInfo().Assembly,
                "BookStore.Portal" // your external assembly's base namespace
            ),
            options.FileProvider
        );
    });

In project.json of your external assembly, add this:

  "resource": "Views/**"

Here's a sample implementation that you can clone and run to see it in action: https://github.com/johnnyoshika/mvc6-view-components

Johnny Oshika
  • 54,741
  • 40
  • 181
  • 275
  • I'd like to have multiple assemblies contributing views and controller logic to a central web app. Any thoughts on how to target an array of FileProviders? – Matthew Dec 22 '15 at 13:42
  • 1
    @Matthew, `CompositeFileProvider` accepts a collection of `FileProviders`, so you can register as many as you want. `new CompositeFileProvider(EmbeddedFileProvider, EmbeddedFileProvider, EmbeddedFileProvider, etc)`. – Johnny Oshika Dec 22 '15 at 14:03
  • @Matthew, if you look at the sample implementation that I linked to above, you will see an example where I'm pulling in views from multiple assemblies. Here is the relevant code: https://github.com/johnnyoshika/mvc6-view-components/blob/master/Web/Startup.cs#L27 – Johnny Oshika Dec 22 '15 at 16:52
  • Nice - for asp.net core final you'll need `"Microsoft.Extensions.FileProviders.Embedded": "1.1.0"` – Sam Feb 15 '17 at 16:01
  • Can you use it in .NET 4.5.1? – Chirag K Feb 16 '17 at 09:27
2

I think the views must live in the main web app unless you want to use some non-standard 3rd party solutions

My understanding is that in beta7 we will be able to package views and other content files in a class library nuget that is created when we build the class library with VS 2015. But my understanding is that when the main web app adds a dependency on such a nuget, the content files will be added to the main web app, ie my views will be added beneath Views/MyModule or something like that so they are usable from the main web application.

At least this is the approach I expect to take so that my views can be read and/or modified later by others.

I think the other option is to precompile the views such that they don't exist on disk as .cshtml files but this would make it more difficult for others to customize the views.

Community
  • 1
  • 1
Joe Audette
  • 35,330
  • 11
  • 106
  • 99
  • We want to ship our modules externally. They should be addable or removable by copying or deleting its folder. So we don't want to make them part of the main project. We don't want to make them a self running web application neither. – Yves Jul 23 '15 at 11:58
  • I temporarily found a solution (also posted it here) and using it at the moment. I will wait for that beta7 feature to give it a try. – Yves Jul 23 '15 at 12:00
2

I achieved this by using IViewLocationExpander and PhysicalFileProvider

I used IFileProvider of the razor's engine to set the root path to the src folder. By appending the assembly name, I get the path of the projects root path.

public class MultiAssemblyViewLocationExpander : IViewLocationExpander
{
    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        var actionContext = (ResultExecutingContext)context.ActionContext;
        var assembly = actionContext.Controller.GetType().Assembly;
        var assemblyName = assembly.GetName().Name;

        foreach (var viewLocation in viewLocations)
            yield return "/" + assemblyName + viewLocation;
    }

    public void PopulateValues(ViewLocationExpanderContext context)
    {

    }
}

And in ConfigureServices method:

services.Configure<RazorViewEngineOptions>(options =>
{
    options.FileProvider = new PhysicalFileProvider(HostingEnvironment.WebRootPath + "..\\..\\");
    options.ViewLocationExpanders.Add(new MultiAssemblyViewLocationExpander ());
});

PS: I didn't test this on a published application. But it will be easy to fix it if some path problems occur ;)

Yves
  • 3,752
  • 4
  • 29
  • 43