Search code examples
c#asp.net-coreasp.net-core-mvcasp.net-core-viewcomponent

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


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:

enter image description here

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?


Solution

  • 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