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

3

20

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?

Enervate answered 12/12, 2015 at 6:1 Comment(0)
E
23

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

Enervate answered 18/12, 2015 at 17:17 Comment(15)
Thanks, I was waiting for this solution a century, now I don't have to copy the same Views across the projects!!! :)Lorenzalorenzana
@Johnny Oshika: Is there a way to load the view components from assemblies automatically? I mean creating like modules that are dumped into a folder called Modules and the application will load automatically all of them without explicitly defining them in code ?Vevina
@user2818430: I haven't tried doing such a thing. The C# classes should be registered automatically based on conventions, but to register the views, you'll probably have to register them on startup. Have you tried it?Enervate
I cannot thank you enough for this answear, I was trying to find a way to replace my old portable areas and it seams the way to do it. I got stucked a little with the "namespace", make sure it is the same as the one defined in "assambleyInfo.cs"Lancer
Do you have any advice on how to add localized resources to this solution? #34891055 Thank youLancer
@RaphaelIsidro: I don't, but it looks like your received an answer to your question.Enervate
Shouldn't the resource entry in project.json include the default patterns, especially the **/*.resxand be a list of strings, not a string: [ "compiler/resources/**/*", "**/*.resx", "Views/**" ]. This way you'll preserve the default functionality.Goines
@MariuszJamro: I don't know. :-) Is that how you're doing it and does it work for you?Enervate
When trying to build the demo project, I am getting this error: Unable to locate Dependency Microsoft.AspNet.FileProviders.Composite >= 1.0.0-* plz help --- oh, nm, had to add that source to nuget: https://www.myget.org/F/aspnetvnext/api/v3/index.jsonTalaria
Nice! This actualyl works without having any hard reference to your ViewComponent project in the Web project. Just Build Output on that ViewComponent project... reference the Nuget package and do the stuff in this post and it all works!!!Talaria
@SerjSagan: That's very interesting, but how are you able to register the EmbeddedFileProvider without referencing the ViewComponent project from your web project? I think you'd get a compiler error.Enervate
The nuget package has your ViewComponent, so just add the nuget package and not a reference to your ViewComponent project and everything compiles just fine.Talaria
My projects are no longer able to find Microsoft.AspNet.FileProviders.Composite any clue what happened to it?Talaria
Somewhere along the way the Type interface changed (writing this as of .netcore2). There is no longer GetTypeInfo(), but you can reference the .Assembly property directly on Type now: typeof(SampleClassInAssembly).Assembly. See: learn.microsoft.com/en-us/dotnet/api/…Vulnerable
If you using .net core v3.x, see my answer below.Pouter
B
1

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.

Biceps answered 12/12, 2015 at 9:23 Comment(5)
Interesting. I'll keep digging to see if there's a workaround.Enervate
@JohnnyOshika I guess the only possible workaround is to manually generate RazorFileInfoCollection class in the class library.Biceps
it looks like there may be some hooks we can tap into? There is a AddPrecompiledRazorViews extension method on IMvcBuilder and it can be called like this: services.AddMvc().AddPrecompiledRazorViews(assembly), but so far I haven't gotten this to work. I tried pre-compiling the views in the class library using a technique described here, strathweb.com/2014/12/…, but so far, no luck.Enervate
@JohnnyOshika I have tried the same technique, but it works only for web application binary, and framework precompiles only on run. The idea is that we need to create RazorFileInfoCollection somewhere in our class library and inject into main assembly, but its hard to implement at the momentBiceps
Yeah, I'm not able to figure it out either. Hopefully someone from the MVC team will weigh in soon: github.com/aspnet/Mvc/issues/3750Enervate
P
1

As of .NetCore v3.x:

  1. [Optional] Remove Microsoft.Extensions.FileProviders.Embedded nuget package
  2. Install Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation nuget package
  3. Call .AddRazorRuntimeCompilation(), e.g: services.AddMvc().AddRazorRuntimeCompilation()
  4. Instead of
services.Configure<RazorViewEngineOptions>(options =>
{
    options.FileProviders.Add(new EmbeddedFileProvider(
         typeof(SampleClassInAssembly).Assembly
    ));
});

Add this:

services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
{
    options.FileProviders.Add(new EmbeddedFileProvider(
         typeof(SampleClassInAssembly).Assembly
    ));
});

And you are good to go.

Related github issue

Pouter answered 11/4, 2020 at 15:5 Comment(1)
Don't forget to set the default.cshtml build action to "Embedded Resource" in the properties, this was the last piece of the puzzle for me.Hydrotherapeutics

© 2022 - 2024 — McMap. All rights reserved.