The other day we [explored using view components in ASP.NET 5][1] - as a very nifty replacement for the old MVC ChildActions. View components allow you to package a piece of functionality into a reusable class, along with an accompanying view, that can be invoke from any other view on demand.
Today let’s take this a step further - and let’s see how you can configure ASP.NET MVC 6, to be able to consume view components not just from the current web project but from external sources - external assemblies too. This way you will be able to share and distribute your view components across multiple projects. This is definitely useful for anyone who has - for example - ever worked on a portal-style applications, where building reusable components is one of the most important development activities.
The requirements 🔗
While the latest version of ASP.NET 5 at the time of writing is RC1, this particular feature that I am going to be using is only available in the latest code (which is going to become RC2 in some - near? - future). For now, in order to be able to follow along, we’ll be pulling the packages from the CI feed at https://www.myget.org/F/aspnetcidev/api/v3/index.json. You can either modify your machine nuget packages sources (although that’s a bit brave), or just add a nuget.config file, pointing to the CI feed, to your solution root:
<?xml version="1.0" encoding="utf-8"?>
<configuration> <packageSources> <add key="aspnetcidev" value="https://www.myget.org/F/aspnetcidev/api/v3/index.json" /> </packageSources> </configuration>
By the way, all of the source code for this article is [available at Github][2].
Set up 🔗
In order to be able to use view components from external assemblies we will rely on a feature called file providers - represented in ASP.NET 5 by an interface IFileProvider. It allows you to instruct ASP.NET 5 from where the files (such as for example MVC views) that should be used within the application are coming from. RoslynCompilationService, when compiling your application, is going to rely on the file provider to discover files.
By default, to find Razor views, ASP.NET MVC 6 preregisters the PhysicalFileProvider, an implementation of IFileProvider that looks at all the files in the base path of your application.
In MVC 6, since RC2 RazorViewEngineOptions, which can be configured at your MVC application startup, will enable you to set multiple IFileProviders that the MVC framework is going to consult (in addition to the default PhysicalFileProvider, or even instead of it - since it can be removed too) when looking for files. At this point it should be pretty self explanatory - normally MVC 6 would only look at the current project to discover views. With the possibility to register extra providers, you can instruct the framework to additionally look at an external assembly and consider the embedded resources from there too.
What happens under the hood, is that the framework actually relies on a new implementation of IFileProvider, called CompositeFileProvider (also introduced in RC2), which is simply able to wrap multiple IFileProviders, and expose the files provided by them using a single common interface.
As mentioned, this change was only introduced recently - if you tried this against the RC1 version of the code, you’d have noticed that only a single IFileProvider was allowed for an MVC application in that version. That obviously made it impossible to efficiently share things such as view components. After all, if you chose an external file provider - pointed it to external assembly - the framework would no longer discover your “web project” views anymore as it would replace the default PhysicalFileProvider.
Code 🔗
So let’s start by creating an empty ASP.NET 5 application. I am going to reference all of the dependencies as latest - and they will come from the CI feed as pre release RC2 builds.
My project.json is shown below. The application is called Reusable.WebApp and it references a class library, created in the same solution, called Reusable.Components.
We are also referencing Microsoft.AspNet.FileProviders.Embedded package, but more on that later.
{
"version": "1.0.0-*",
"compilationOptions": {
"emitEntryPoint": true
},
"dependencies": {
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-*",
"Microsoft.AspNet.Mvc": "6.0.0-*",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-*",
"Microsoft.AspNet.Diagnostics": "1.0.0-*",
"Reusable.Components": "1.0.0",
"Microsoft.AspNet.FileProviders.Embedded": "1.0.0-*"
},
"commands": {
"web": "Reusable.WebApp -server Microsoft.AspNet.Server.Kestrel -server.urls http://localhost:5000"
},
"frameworks": {
"dnx451": { },
"dnxcore50": { }
},
"exclude": [
"wwwroot"
],
"publishExclude": [
"**.user",
"**.vspscc"
]
}
Our reusable view component is going to something very simple - something that we already built in [the old post][1]. Of course no one wants to read old posts, so here is the C# code:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public interface IProductService
{
Task<Product[]> GetPromotedProducts();
}
public class ProductService : IProductService
{
public Task<Product[]> GetPromotedProducts()
{
//for simplicity data is in memory
var data = new[]
{
new Product
{
Name = "Etape: 20 Great Stages from the Modern Tour de France",
Price = 9.90m
},
new Product
{
Name = "Anarchy Evolution: Faith, Science and Bad Religion in a World Without God",
Price = 8.90m
},
new Product
{
Name = "The Bright Continent: Breaking Rules and Making Changes in Modern Africa",
Price = 12.50m
}
};
return Task.FromResult(data);
}
}
[ViewComponent(Name = "PromotedProducts")]
public class PromotedProductsViewComponent : ViewComponent
{
private readonly IProductService _productService;
public PromotedProductsViewComponent()
{
_productService = new ProductService();
}
public async Task<IViewComponentResult> InvokeAsync()
{
var products = await _productService.GetPromotedProducts();
return View(products);
}
}
And the corresponidng Razor view, located under /Views/Shared/Components/PromotedProducts/Default.cshtml.
@model IEnumerable<Product>
<div class="panel panel-default">
<div class="panel-heading">
Promoted products
</div>
<ul class="list-group">
@foreach (var product in Model)<br /> {</p>
<li class="list-group-item">
@product.Name - @product.Price
</li>
<p>
} </ul> </div>
<p>
```
</p>
<p>
As mentioned, the difference is that instead of adding all that code into an ASP.NET Web Application, we are actually going to add it to a class library instead (that <em>Reusable.Components</em> project I mentioned earlier).<br /> This class library needs to reference <em>Microsoft.AspNet.Mvc</em> package in the <em>project.json</em> in order to be able to contain a class inherting from <em>ViewComponent</em>, otherwise it wouldn’t compile, but that’s about it - it’s not a web project by any means.
</p>
<p>
Now, in order for the Razor views to be embedded as resources inside this library, we need an additional node in the <em>project.json</em>, called <em>resources</em> where we’ll specify the path to our library-specific views. This is shown in the next listing, illustrating the <em>project.json</em> of that class library:
</p>
<p>
[crayon lang="javascript"]<br /> {<br /> "version": "1.0.0-*",<br /> "description": "Reusable.Components Class Library",<br /> "authors": [ "filip" ],<br /> "tags": [ "" ],<br /> "projectUrl": "",<br /> "licenseUrl": "",<br /> "frameworks": {<br /> "net451": { },<br /> "dotnet5.5": {<br /> "dependencies": {<br /> "Microsoft.CSharp": "4.0.1-*",<br /> "System.Collections": "4.0.11-*",<br /> "System.Linq": "4.0.1-*",<br /> "System.Runtime": "4.0.21-*",<br /> "System.Threading": "4.0.11-*"<br /> }<br /> }<br /> },<br /> "dependencies": {<br /> "Microsoft.AspNet.Mvc": "6.0.0-*"<br /> },<br /> "resource": "Views/**"<br /> }<br /> ```
</p>
<p>
Note that since we are using the CI feed, and every package involved is latest and greatest, the target framework moniker needs to be <em>dotnet5.5</em> not <em>dotnet5.4</em> anymore, but that’s just a detail.
</p>
<p>
Finally, to stitch it all together we need to instruct ASP.NET MVC 6 to use our external components by adding an <em>EmbeddedFileProvider</em>, pointing to our <em>Reusable.Components</em> class library as one of the available file providers in the web application. This is done at application startup, inside the <em>ConfigureServices</em> method of the <em>Startup</em> class.
</p>
<p>
```csharp
<br /> public void ConfigureServices(IServiceCollection services)<br /> {<br /> services.AddMvc();
</p>
<p>
services.Configure<RazorViewEngineOptions>(options =><br /> {<br /> options.FileProviders.Add(new EmbeddedFileProvider(<br /> typeof(PromotedProductsViewComponent).GetTypeInfo().Assembly,<br /> "Reusable.Components"<br /> ));<br /> });<br /> }<br /> ```
</p>
<p>
Because the <em>FileProviders</em> on the <em>RazorViewEngineOptions</em> is now (in RC2) a collection, we can now inject an extra provider without having to worry about losing the default <em>PhysicalFileProvider</em>.
</p>
<p>
And that’s about it. You can now add the view component (a call to <em>@await Component.InvokeAsync("PromotedProducts")</em>) to any page in your application and use as if it was locally present inside the web app. There are surely plenty of great uses cases of being able to package reusable components this way and use them in various web applications.
</p>
<p>
As a reminder, all of the source code for this article is <a href="https://github.com/filipw/aspnet5-reusable-components">available at Github</a>
</p>
<h3>
Footnote
</h3>
<p>
You can also use this approach in ASP.NET MVC 6 RC1 if you just pull in a CI version (so RC2-compatible) of <em>Microsoft.AspNet.FileProviders.Composite</em> package. It will contain a <em>CompositeFileProvider</em>, which will let you to bundle together the default file provider (<em>PhysicalFileProvider</em>) and an external one such as an <em>EmbeddedFileProvider</em> pointing to your class library with reusabel components. Then you can simply set this instance of <em>CompositeFileProvider</em> on the <em>FileProvider</em> property of <em>RazorViewEngineOptions</em>. The reason for this is that - as mentioned already - MVC 6 RC1 had only a single file provider, while MVC 6 RC2 allows multiple - hence the difference in the apporach.
</p>
<p>
```csharp
<br /> public void ConfigureServices(IServiceCollection services)<br /> {<br /> services.AddMvc();<br /> services.Configure<RazorViewEngineOptions>(options =><br /> {<br /> options.FileProvider = new CompositeFileProvider(<br /> new EmbeddedFileProvider(<br /> typeof(PromotedProductsViewComponent).GetTypeInfo().Assembly,<br /> "Reusable.Components"), //new one<br /> options.FileProvider //default one<br /> );<br /> });<br /> }<br /> ```
</p>
[1]: /2015/07/viewcomponents-asp-net-5-asp-net-mvc-6/
[2]: https://github.com/filipw/aspnet5-reusable-components