How to bundle resources for ASP.NET MVC areas? - asp.net-mvc

How would you do the resource bundling for asp.net mvc areas? Is this regulated by the ASP.NET MVC framework just like the AreaRegistration for routes?
I could make a BundleConfig class inside the area and call this from the global BundleConfig inside the App_Start folder but this doens't feel good to me.
I can't find any information on this subject. Any help our thoughts are appreciated.

I was hoping this was somehow more regulated - but after diving into the framework code the answer to this is negative.
What I decided to do is the following:
Solution Structure
Areas:
Admin
RouteConfig.cs
BundleConfig.cs
AdminAreaRegistration.cs
RouteConfig.cs
internal static class RouteConfig
{
internal static void RegisterRoutes(AreaRegistrationContext context)
{
//add routes
}
}
BundleConfig.cs
internal static class BundleConfig
{
internal static void RegisterBundles(BundleCollection bundles)
{
//add bundles
}
}
AdminAreaRegistration.cs
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
RegisterRoutes(context);
RegisterBundles();
}
private void RegisterRoutes(AreaRegistrationContext context)
{
RouteConfig.RegisterRoutes(context);
}
private void RegisterBundles()
{
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}

Your question implies that you have separate scripts & css folders for each area? This is unconventional (though perfectly plausible). Or perhaps you have a single scripts folder at the route level and you have split that into sub folders for each Area? Either way you will have to do something only slightly different to get Area specific bundles.
MVC4 applications come with a static BundleConfig class that lives in the App_Start folder. The bundle is then initialized from the Global.asax. If you are not working with a MVC4 project either upgrade or just start an out of the box MVC4 project to observe the layout of these files.
Bundles are configured simply by declaring a virtual path, (from which the bundle can be referenced) then specifying the files you wish to be bundled. The files to be bundled can be specified by listing the filename explicitly, filename character matching, or specifying a directory in which all files should be included.
To begin with, I would just use this global BundleConfig to specify bundles for all of your areas. If this does not scale for you or becomes unwieldy, you can always break it out later.
Specify which files should be included. You should prefix the bundle's virtual path with the Area that it is for. Then it will be easy to reference using the Area name from your views - most likely your _Layout.cshtml.
For example, here we have two Areas each with distinct scripts: User and Group.
App_Start/BundleConfig.cs
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
/// Bundle For User Area
bundles.Add(new ScriptBundle("~/user/bundles/scripts").Include(
"~/Scripts/User/myuserscript1.js",
"~/Scripts/User/myuserscript2.js"));
/// Bundle For Group Area
bundles.Add(new ScriptBundle("~/group/bundles/scripts").Include(
"~/Scripts/Group/mygroupscript1.js",
"~/Scripts/Group/mygroupscript2.js"));
}
}
You can then use the Scripts.Render() on your main _Layout.cshtml to render the correct Area bundle, depending on which area the user is currently viewing. To do this you first need to get the current Area like so:
Views/Shared/_Layout.cshtml:
<head>
#{
var currentArea = (ViewContext.RouteData.DataTokens["area"]
?? String.Empty).ToString().ToLower();
}
#Scripts.Render("~/" + currentArea + "/bundles/scripts")
</head>
EDIT
If you really want to manage your bundle from within your Area, then the Area registration would be a good place to do so. The BundleTable static property referenced in the BundleConfig is global so it can be referenced anywhere. This code compiles but I haven't tested it. It is for an Area called Test:
Areas/Test/TestAreaRegistration.cs
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Test_default",
"Test/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
BundleTable.Bundles.Add(new Bundle("~/test/bundles/scripts").Include(
"~/Areas/Test/Scripts/jquery.js"));
}

Related

ASP.NET Core Razor SDK Class Library - Area Views Not Within Areas Directory

I'm trying to use the new Razor SDK to include my views within my class libraries where each class library is an MVC Area. If I include the views with an Areas directory in my class library e.g.
/MyLibrary/Areas/MyLibrary/Views/Home/Index.cshtml
Then it loads fine. However I don't like that I have to place them inside an Areas directory ideally the path would be:
/MyLibrary/Views/Home/Index.cshtml
I'm guessing I would have to use a view location expander to achieve this. However I don't know how to achieve this for a view which is contained within a class library and not within the application. So far I have come up with:
public class AreaViewLocationExpander : IViewLocationExpander {
public virtual IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations) {
return viewLocations.Concat(new[] {
"../{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension
});
}
public virtual void PopulateValues(ViewLocationExpanderContext context) { }
}
This throws the error:
InvalidOperationException: The view 'Index' was not found. The
following locations were searched:
/Areas/MyLibrary/Views/Home/Index.cshtml
/Areas/MyLibrary/Views/Shared/Index.cshtml
/Views/Shared/Index.cshtml
/Pages/Shared/Index.cshtml
/../MyLibrary/Views/Home/Index.cshtml
But I'd imagine even if it did work locally, it wouldn't in production or when the library is packaged up in a NuGet package.
I'd appreciate it if someone could show me how this can be achieved. Thanks
Step 1
From #pranavkm on GitHub:
The Razor Sdk uses well-known MSBuild metadata to calculate project
relative paths. You may set the Link metadata for a file and Razor
would use that. For instance, adding this to your project file would
update all cshtml files to have a view engine path with the project
name as a prefix:
<Target Name="UpdateTargetPath" BeforeTargets="AssignRazorGenerateTargetPaths">
<ItemGroup>
<RazorGenerate Include="#(RazorGenerate)" Link="$(TargetName)\%(RazorGenerate.RelativeDir)%(RazorGenerate.FileName)%(RazorGenerate.Extension)" />
</ItemGroup>
</Target>
This does not work with runtime compilation - i.e. if you were to
apply this to Application.csproj, it'll work for build time compiled
views, but runtime compiled views would continue to use
/Views/Home/Index.cshtml
Step 2
You would then need to add the following IViewLocationExpander:
public class AreaViewLocationExpander : IViewLocationExpander {
public virtual IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations) {
return viewLocations.Concat(new[] {
"/{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension
});
}
public virtual void PopulateValues(ViewLocationExpanderContext context) { }
}

ASP.NET MVC ScriptBundle: change rendered output

Is it possible to change the rendered output of a ScriptBundle in ASP.NET MVC? When configuring the bundles with EnableOptimizations = false, the output for each script included in the bundle is something like this:
<script src="~/Scripts/path/to/script"></script>
I would like to change this "template" based on the ScriptBundle (for all bundles would also be fine). Is there a way to change this?
Have a look at the below code, which will always give fresh file.
using System.IO;
using System.Web;
using System.Web.Hosting;
using System.Web.Optimization;
namespace TestProj
{
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/common").Include("~/Scripts/CommonScripts.js").WithLastModifiedToken());
BundleTable.EnableOptimizations = false;
}
}
internal static class BundleExtensions
{
public static Bundle WithLastModifiedToken(this Bundle sb)
{
sb.Transforms.Add(new LastModifiedBundleTransform());
return sb;
}
public class LastModifiedBundleTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
foreach (var file in response.Files)
{
var lastWrite = File.GetLastWriteTime(HostingEnvironment.MapPath(file.IncludedVirtualPath)).Ticks.ToString();
file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", lastWrite);
}
}
}
}
}
The output will be
"/Scripts/CommonScripts.js?v=636180193140000000"
Here i am adding the last modified date of the file as query parameter. So whenever the file changes browser will get the fresh file all the time. Or instead of last updated time you can add version like '1.0.0' in the query parameter.
I wrestled with MVC bundling for a long time too. It was great to begin with, but you start to fight a losing battle with it when you need to do anything out of the ordinary (like your question).
I'm sorry this doesn't directly answer your question, but I moved to WebPack because of issues like this, and have never looked back.
https://webpack.js.org

ASP.NET MVC Boilerplate: Migrating from AutoFac to Unity, cannot get Services DI working again

Current project:
ASP.NET MVC 5 boilerplate (Github)
Switching Autofac out for Unity
When I switch the DI from AutoFac to Unity, I am unable to get the Services built into the boilerplate (robots.txt, sitemap.xml) back up and running. In particular, I am unable to translate the Autofac entries for these services to the appropriate Unity entries.
My HomeController default constructor is unchanged from the default, at least for robots.txt, which I am doing the litmus test on:
private readonly IRobotsService _robotsService;
public HomeController(IRobotsService robotsService) {
_robotsService = robotsService;
}
The robots.txt method in my HomeController is similarly default for the boilerplate:
[NoTrailingSlash]
[OutputCache(CacheProfile = CacheProfileName.RobotsText)]
[Route("robots.txt", Name = HomeControllerRoute.GetRobotsText)]
public ContentResult RobotsText() {
Trace.WriteLine($"robots.txt requested. User Agent:<{Request.Headers.Get("User-Agent")}>.");
var content = _robotsService.GetRobotsText();
return Content(content, ContentType.Text, Encoding.UTF8);
}
The IRobotsService and RobotsService files are also default for the boilerplate - they are completely unmodified (aside from removing comments for brevity):
namespace Project.Website.Services {
public interface IRobotsService {
string GetRobotsText();
}
}
namespace Project.Website.Services {
using Boilerplate.Web.Mvc;
using Constants;
using System.Text;
using System.Web.Mvc;
public sealed class RobotsService : IRobotsService {
private readonly UrlHelper _urlHelper;
public RobotsService(UrlHelper urlHelper) => _urlHelper = urlHelper;
public string GetRobotsText() {
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("user-agent: *");
stringBuilder.AppendLine("disallow: /error/");
stringBuilder.Append("sitemap: ");
// Commented out so it wouldn't trigger the sitemap, which is not active:
//stringBuilder.AppendLine(_urlHelper.AbsoluteRouteUrl(HomeControllerRoute.GetSitemapXml).TrimEnd('/'));
return stringBuilder.ToString();
}
}
}
The original Startup.Container.cs for Autofac is quite extensive, but the robots.txt service is injected by:
builder.RegisterType<RobotsService>().As<IRobotsService>().InstancePerRequest();
When my UnityConfig.cs file has the following:
container.RegisterType<RobotsService>(new TransientLifetimeManager());
I get
The current type, JCI_Vernon.Website.Services.IRobotsService, is an interface and cannot be constructed. Are you missing a type mapping?
Which pretty well tells me I have to include IRobotsService, but when my UnityConfig file has the following:
container.RegisterType<IRobotsService, RobotsService>(new TransientLifetimeManager());
I get
The current type, System.Web.HttpContextBase, is an abstract class and cannot be constructed. Are you missing a type mapping?
I am unsure as to where I am going wrong, as all other Unity DI in my project is configured by using one of these two variants.
Any assistance would be greatly appreciated.
Edit: Including the Unity files from my primary project (the visible website).
UnityMvcActivator.cs:
[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(JCI_Vernon.Website.UnityMvcActivator), nameof(JCI_Vernon.Website.UnityMvcActivator.Start))]
[assembly: WebActivatorEx.ApplicationShutdownMethod(typeof(JCI_Vernon.Website.UnityMvcActivator), nameof(JCI_Vernon.Website.UnityMvcActivator.Shutdown))]
namespace JCI_Vernon.Website {
using System.Linq;
using System.Web.Mvc;
using Unity.AspNet.Mvc;
/// <summary>
/// Provides the bootstrapping for integrating Unity with ASP.NET MVC.
/// </summary>
public static class UnityMvcActivator {
/// <summary>
/// Integrates Unity when the application starts.
/// </summary>
public static void Start() {
FilterProviders.Providers.Remove(FilterProviders.Providers.OfType<FilterAttributeFilterProvider>().First());
FilterProviders.Providers.Add(new UnityFilterAttributeFilterProvider(UnityConfig.Container));
DependencyResolver.SetResolver(new UnityDependencyResolver(UnityConfig.Container));
// TODO: Uncomment if you want to use PerRequestLifetimeManager
// Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(UnityPerRequestHttpModule));
}
/// <summary>
/// Disposes the Unity container when the application is shut down.
/// </summary>
public static void Shutdown() {
UnityConfig.Container.Dispose();
}
}
}
UnityConfig.cs:
namespace JCI_Vernon.Website {
using Data;
using Domain;
using Identity;
using Microsoft.AspNet.Identity;
using Services;
using Store;
using System;
using System.Web;
using System.Web.Mvc;
using Unity;
using Unity.Injection;
using Unity.Lifetime;
using Unity.Mvc5;
public static class UnityConfig {
public static IUnityContainer Container { get; internal set; }
public static void RegisterComponents() {
var container = new UnityContainer();
container.RegisterType<IUnitOfWork, UnitOfWork>(new HierarchicalLifetimeManager(), new InjectionConstructor("DefaultConnection"));
container.RegisterType<IUserStore<IdentityUser, Guid>, UserStore>(new TransientLifetimeManager());
container.RegisterType<RoleStore>(new TransientLifetimeManager());
container.RegisterInstance<HttpContextBase>(new HttpContextWrapper(HttpContext.Current), new TransientLifetimeManager());
container.RegisterType<IRobotsService, RobotsService>(new Unity.AspNet.Mvc.PerRequestLifetimeManager());
//container.RegisterType<ISitemapService, SitemapService>(new InjectionConstructor());
//container.RegisterType<ISitemapPingerService, SitemapPingerService>(new InjectionConstructor());
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
}
}
In my UnityMvcActivator.cs, I have had that one PerRequestLifetimeManager line both commented and uncommented with every change, no difference observed. Any attempt to use PerRequestLifetimeManager within UnityConfig.cs without Unity.Mvc (as using Unity.AspNet.Mvc;) failed.
Changing UnityConfig.cs to include Unity.AspNet.Mvc caused mass borkage: while I was able to get PerRequestLifetimeManager to be accepted without obvious Intellisense error, UnityMvcActivator.cs suddenly couldn’t resolve its UnityConfig.Container entries without a very odd entry at the top of UnityConfig.cs:
public static IUnityContainer Container { get; internal set; }
And the SetResolver in UnityConfig.cs needed to explicitly state new Unity.Mvc5.UnityDependencyResolver(container) in order to not trigger Intellisense confusion.
Plus, when run, the following error occurred:
Could not load file or assembly 'Unity.Abstractions, Version=3.1.3.0, Culture=neutral, PublicKeyToken=6d32ff45e0ccc69f' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
Why it is trying to target a v3.x of Unity despite the entire solution having been created under v5.x is causing my grey matter no end of meltdown. And yes, I did a full clean and rebuild of the entire solution, plus individual projects.
Edit 2:
May have come across an interesting wrinkle. On a lark, I decided to do a full reinstallation of all NuGet packages, a refresh of sorts. Naturally, when you do an upgrade or reinstall of Unity, it tries to overwrite your unity files, which is why you always need to have your UnityConfig.cs backed up otherwise your registrations will vanish. Happens to me with every. Single. F##cking. Project. So annoying.
So anyhow, I did a full refresh, and my UnityConfig.cs suddenly underwent a major change. Before it was as above, including all upgrades within v5, but the refresh provided me with the following (comments removed for brevity):
namespace JCI_Vernon.Website {
using System;
using Unity;
public static class UnityConfig {
#region Unity Container
private static Lazy<IUnityContainer> container =
new Lazy<IUnityContainer>(() => {
var container = new UnityContainer();
RegisterTypes(container);
return container;
});
public static IUnityContainer Container => container.Value;
#endregion
public static void RegisterTypes(IUnityContainer container) {
// TODO: Register your type's mappings here.
// container.RegisterType<IProductRepository, ProductRepository>();
}
}
}
Ya, weird. Major change with no clue why. The old version works just fine, it just blows its cookies all over the specific type mapping this post is about.
Plus, I have to idea what to change the Global.cs entry to in order to load my type mappings, as just using the obvious (changing UnityConfig.RegisterComponents(), which cannot be found, to UnityConfig.RegisterTypes()) does not make any sense -- how do I pass in the container?
There are a couple of issues here. First of all, this line:
container.RegisterType<RobotsService>(new TransientLifetimeManager());
is not the equivalent of:
builder.RegisterType<RobotsService>().As<IRobotsService>().InstancePerRequest();
It should instead be:
container.RegisterType<IRobotsService, RobotsService>(new TransientLifetimeManager());
Keep in mind Autofac type mappings use the concrete type first, and then the interface type. This is backward from most other DI containers.
The last error message indicates you need to register HttpContextBase with Unity. You do that by wrapping HttpContext.Current with HttpContextWrapper.
container.RegisterInstance<HttpContextBase>(new HttpContextWrapper(HttpContext.Current), new TransientLifetimeManager());

MEF - notify when plugins are loaded / unloaded

I have a simple asp mvc app which uses MEF, and there is a route which can be accessed by admins to refresh the directory catalog and compose parts, however one thing I am trying to find out how to do is notify some code when a plugin is loaded / unloaded.
The scenario is that when plugins are loaded they register the routes they need, however when they are unloaded I need them to unload their routes, as subsequent refreshes try to re-register the routes and it bombs.
Are there any events which I can hook into from the MEF objects?
The plugin container is something like:
[ImportMany(typeof(ISomePluginInterface))]
IEnumerable<ISomePluginInterface> Plugins {get; private set;}
Each ISomePluginInterface has something like:
public interface ISomePluginInterface
{
public void PluginLoaded();
public void PluginUnloaded();
}
This is similar in theory to this Stackoverflow question and this was my answer. In your case, you have a similar need, you want to fire an event when the plugin is started, and clean up when it is no longer needed.
Using the same concept, you can use the InterceptingCatalog to register routes, but I wouldn't make it an explicit part of the interface definition to do so, instead, you need to look at how your components fit together as a whole, e.g., if the operations for registering routes won't be used for all plugins, what is the purpose of them existing in the interface definition. You could break out the route registration into a separate interface, the IRouteRegistrar, and use intercepting strategies to automatically call the appropriate registration method when the plugin is used for the first time, e.g., I could break out the interface into:
public interface IPlugin
{
void SomeOperation();
}
public interface IRouteRegistrar : IDisposable
{
void RegisterRoutes();
}
The latter interface does the work of registering routes, and we use the Dispose pattern to ensure that it is cleaned up after it is finished with. Therefore, A sample plugin could resemble:
[Export(typeof(IPlugin))]
public class MyPlugin : IPlugin, IRouteRegistrar
{
public void SomeOperation() { }
public void RegisterRoutes()
{
// Register routes here...
}
protected virtual Dispose(bool disposing)
{
if (disposing)
{
// Unregister routes here...
}
}
void IDisposable.Dispose()
{
Dispose(true);
}
}
I only export as an IPlugin, but I ensure my plugin also implements the IRouteRegistrar. The way we use that, is with a strategy:
public class RouteRegistrarStrategy : IExportedValueInteceptor
{
public object Intercept(object value)
{
var registrar = value as IRouteRegistrar;
if (registrar != null)
registrar.RegisterRoutes();
return value;
}
}
Now, only if the plugin supports that interface will it register routes. This also enables you to apply the route registration interface to other plugins which could be used in a different way. You gain a bit more flexibility. To use that strategy in code, you need to add the MefContrib project to your app, and do a little more wire up:
var catalog = new DirectoryCatalog(".\bin");
var config = new InterceptionConfiguration().AddInterceptor(new RouteRegistrarStrategy());
var interceptingCatalog = new InterceptingCatalog(catalog, configuration);
var container = new CompositionContainer(interceptingCatalog);

Avoiding missing views in ASP.NET MVC

When testing an ASP.NET MVC 2 application I hit a problem when a view could not be located.
Looking at the code I realised that the aspx file for the view had not been added to the source control repository. On this project that's quite easy to do as we use StarTeam for source control and it doesn't show new folders when checking in. This view was for a new controller and so a new folder was created for it and it was therefore missed.
Our build server (using Hudson/MSBuild) didn't pick up on this, as the code still builds fine with the aspx file missing. Our controller unit tests test the ActionResults which obviously still pass without the view there.
This got picked up in system testing but how can I catch this earlier (ideally on the build server).
Thanks in advance
You can write unit tests that test the actual view, and then if the unit test doesn't pass on the build server, you know you have a problem. To do this, you can use a framework such as this:
http://blog.stevensanderson.com/2009/06/11/integration-testing-your-aspnet-mvc-application/
With this you can write unit tests such as this (from the post)
[Test]
public void Root_Url_Renders_Index_View()
{
appHost.SimulateBrowsingSession(browsingSession => {
// Request the root URL
RequestResult result = browsingSession.ProcessRequest("/");
// You can make assertions about the ActionResult...
var viewResult = (ViewResult) result.ActionExecutedContext.Result;
Assert.AreEqual("Index", viewResult.ViewName);
Assert.AreEqual("Welcome to ASP.NET MVC!", viewResult.ViewData["Message"]);
// ... or you can make assertions about the rendered HTML
Assert.IsTrue(result.ResponseText.Contains("<!DOCTYPE html"));
});
}
What version of StarTeam are you running? In StarTeam 2008 (not sure when this feature was first added) within a selected project/view, you can select from the menu Folder Tree->Show Not-In-View Folders. This will show folders you have on local disk that have not been added to the project (they will appear with the folder icon colored white).
This is an old question, but if anyone still looking for this you ought to try SpecsFor.Mvc by Matt Honeycutt.
Not only it can be used to make sure the Views are properly included/added in the source control, it can even do integration test to make sure those Views are valid.
Link to its website: http://specsfor.com/SpecsForMvc/default.cshtml
Link to the nuget package: https://www.nuget.org/packages/SpecsFor.Mvc/
Link to github: https://github.com/MattHoneycutt/SpecsFor
Here is a code snippet taken from the website showing how to use it.
public class UserRegistrationSpecs
{
public class when_a_new_user_registers : SpecsFor<MvcWebApp>
{
protected override void Given()
{
SUT.NavigateTo<AccountController>(c => c.Register());
}
protected override void When()
{
SUT.FindFormFor<RegisterModel>()
.Field(m => m.Email).SetValueTo("test#user.com")
.Field(m => m.UserName).SetValueTo("Test User")
.Field(m => m.Password).SetValueTo("P#ssword!")
.Field(m => m.ConfirmPassword).SetValueTo("P#ssword!")
.Submit();
}
[Test]
public void then_it_redirects_to_the_home_page()
{
SUT.Route.ShouldMapTo<HomeController>(c => c.Index());
}
[Test]
public void then_it_sends_the_user_an_email()
{
SUT.Mailbox().MailMessages.Count().ShouldEqual(1);
}
[Test]
public void then_it_sends_to_the_right_address()
{
SUT.Mailbox().MailMessages[0].To[0].Address.ShouldEqual("test#user.com");
}
[Test]
public void then_it_comes_from_the_expected_address()
{
SUT.Mailbox().MailMessages[0].From.Address.ShouldEqual("registration#specsfor.com");
}
}
}

Resources