Asp.Net MVC Bundling, best way to detect missing file - asp.net-mvc

I've just chased a bug that was due to a missing javascript file, it was failing silently.
The minified version of the file was present but not the full version, a link isn't rendered on the client (which I was expecting) but I don't get an exception either. I'd like to know if the file isn't present.
(just to be clear, the bundle didn't try to included the minified version, it tried to include the full version, but the minified version was present in the script directory)
Do I have to write something custom to detect this or does MVC have anything built in to report this?
thanks

I came up to using the following extension methods for Bundle:
public static class BundleHelper
{
[Conditional("DEBUG")] // remove this attribute to validate bundles in production too
private static void CheckExistence(string virtualPath)
{
int i = virtualPath.LastIndexOf('/');
string path = HostingEnvironment.MapPath(virtualPath.Substring(0, i));
string fileName = virtualPath.Substring(i + 1);
bool found = Directory.Exists(path);
if (found)
{
if (fileName.Contains("{version}"))
{
var re = new Regex(fileName.Replace(".", #"\.").Replace("{version}", #"(\d+(?:\.\d+){1,3})"));
fileName = fileName.Replace("{version}", "*");
found = Directory.EnumerateFiles(path, fileName).FirstOrDefault(file => re.IsMatch(file)) != null;
}
else // fileName may contain '*'
found = Directory.EnumerateFiles(path, fileName).FirstOrDefault() != null;
}
if (!found)
throw new ApplicationException(String.Format("Bundle resource '{0}' not found", virtualPath));
}
public static Bundle IncludeExisting(this Bundle bundle, params string[] virtualPaths)
{
foreach (string virtualPath in virtualPaths)
CheckExistence(virtualPath);
return bundle.Include(virtualPaths);
}
public static Bundle IncludeExisting(this Bundle bundle, string virtualPath, params IItemTransform[] transforms)
{
CheckExistence(virtualPath);
return bundle.Include(virtualPath, transforms);
}
}
This way you don't have to call your helper method PreCheck() explicitly. It also supports ASP.NET's wildcards {version} and *:
bundles.Add(new ScriptBundle("~/test")
.IncludeExisting("~/Scripts/jquery/jquery-{version}.js")
.IncludeExisting("~/Scripts/lib*")
.IncludeExisting("~/Scripts/model.js")
);

I can't believe this "anti-pattern" exists! If you see no errors, there are no errors!
Anyway, I like the solution above. Another would be to output the link / script even if it is missing when BundleTable.EnableOptimizations is false -- this is a very obvious thing to try when debugging, and then it will be obvious in the various inspectors / debuggers on browsers which file is missing. This seemed like such an obvious thing for debugging thing that I spent hours without realizing there were missing files. The other way, silently ignoring missing parts of the bundle, is so wrong that it reinforced my horrible debugging session.
Well, this won't bite me twice -- trauma endures.

Another way using BundleTable.VirtualPathProvider wrapper:
public class VirtualPathProviderExt : VirtualPathProvider
{
private readonly VirtualPathProvider _provider;
public VirtualPathProviderExt(VirtualPathProvider provider)
{
_provider = provider;
}
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return _provider.CombineVirtualPaths(basePath, relativePath);
}
public override ObjRef CreateObjRef(Type requestedType)
{
return _provider.CreateObjRef(requestedType);
}
public override bool DirectoryExists(string virtualDir)
{
return _provider.DirectoryExists(virtualDir);
}
public override bool Equals(object obj)
{
return _provider.Equals(obj);
}
private static readonly Regex _ignorePathsRegex = new Regex(#"\.debug\.\w+$|^~/bundle.config$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public override bool FileExists(string virtualPath)
{
var result = _provider.FileExists(virtualPath);
if (!result && !_ignorePathsRegex.IsMatch(virtualPath))
{
Logger.Instance.Log(RecType.Error, "Bundle file not found: " + virtualPath);
}
return result;
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return _provider.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override string GetCacheKey(string virtualPath)
{
return _provider.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return _provider.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
return _provider.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return _provider.GetFileHash(virtualPath, virtualPathDependencies);
}
public override int GetHashCode()
{
return _provider.GetHashCode();
}
public override object InitializeLifetimeService()
{
return _provider.InitializeLifetimeService();
}
public override string ToString()
{
return _provider.ToString();
}
}
Bundle helper:
public static class BundleHelpers
{
public static void InitBundles()
{
if (!(BundleTable.VirtualPathProvider is VirtualPathProviderExt))
{
BundleTable.VirtualPathProvider = new VirtualPathProviderExt(BundleTable.VirtualPathProvider);
}
}
}
And run BundleHelpers.InitBundles() in BundleConfig.cs:
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
BundleHelpers.InitBundles();
...

I changed the code a bit. Instead of throwing an error it will not add any bundle file. this is required if you are using same common bundle config for multiple projects
public static class BundleHelper
{
private static bool CheckExistence(string virtualPath)
{
int i = virtualPath.LastIndexOf('/');
string path = HostingEnvironment.MapPath(virtualPath.Substring(0, i));
string fileName = virtualPath.Substring(i + 1);
bool found = Directory.Exists(path);
if (found)
{
if (fileName.Contains("{version}"))
{
var re = new Regex(fileName.Replace(".", #"\.").Replace("{version}", #"(\d+(?:\.\d+){1,3})"));
fileName = fileName.Replace("{version}", "*");
found = Directory.EnumerateFiles(path, fileName).Where(file => re.IsMatch(file)).FirstOrDefault() != null;
}
else // fileName may contain '*'
found = Directory.EnumerateFiles(path, fileName).FirstOrDefault() != null;
}
return found;
//if (!found)
//throw new ApplicationException(String.Format("Bundle resource '{0}' not found", virtualPath));
}
public static Bundle IncludeExisting(this Bundle bundle, params string[] virtualPaths)
{
foreach (string virtualPath in virtualPaths)
if (CheckExistence(virtualPath))
{
bundle.Include(virtualPath);
}
return bundle;
}
public static Bundle IncludeExisting(this Bundle bundle, string virtualPath, params IItemTransform[] transforms)
{
if (CheckExistence(virtualPath))
bundle.Include(virtualPath, transforms);
return bundle;
}
}

Related

Load ASP.NET MVC views and controllers code from database

I have a system in which the end-user is a developer who can create ASP.NET MVC views/controllers and run them on the fly.
Currently, I have two database tables, one to store the view name and code and other to store controller code in C#. I can compile the build an assembly and save a dll file on the server folder.
Step 1: I added a custom controller factory to load my controller from the database, having an area in the project named (QZone).
public class QS_DynamicControllerFactory : DefaultControllerFactory//, IController
{
QS_DBConnection _db = new QS_DBConnection();
public QS_DynamicControllerFactory() { }
public override IController CreateController(RequestContext requestContext, string controllerName)
{
return (requestContext.RouteData.DataTokens["area"] != null &&
requestContext.RouteData.DataTokens["area"].ToString().ToLower() == "qzone") ?
QGetControllerInstance(controllerName) : base.CreateController(requestContext, controllerName);
}
internal IController QGetControllerInstance(string controllerName)
{
//load controller from the database and compile it then return an instance
}
public override void ReleaseController(IController controller)
{
base.ReleaseController(controller);
}
}
Step 2: I created a VirtualPathProvider, VirtualFile
QS_VirtualPathProvider class:
public class QS_VirtualPathProvider : VirtualPathProvider
{
public QDynamicView GetVirtualData(string viewPath)
{
QS_DBConnection _db = new QS_DBConnection();
QDynamicView view = (from v in _db.QDynamicViews
where v.Name.ToLower() == "TestView.cshtml".ToLower()//viewPath.ToLower()
select v).SingleOrDefault();
return view;
}
private bool IsPathVirtual(string virtualPath)
{
var path = (VirtualPathUtility.GetDirectory(virtualPath) != "~/") ? VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(virtualPath)) : VirtualPathUtility.GetDirectory(virtualPath);
if (path.ToLower().Contains("/qzone/"))
return true;
else
return false;
}
public override bool FileExists(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{
QS_VirtualFile file = (QS_VirtualFile)GetFile(virtualPath);
bool isExists = file.Exists;
return isExists;
}
else
return Previous.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{
QDynamicView vw = GetVirtualData(virtualPath);
var bytes = Encoding.ASCII.GetBytes(vw.ViewCode);
return new QS_VirtualFile(virtualPath, bytes);
}
else
return Previous.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsPathVirtual(virtualPath))
{
return null;
}
else
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
if (IsPathVirtual(virtualPath))
return Guid.NewGuid().ToString();
return base.GetFileHash(virtualPath, virtualPathDependencies);
}
}
QS_VirtualFile class:
public class QS_VirtualFile : VirtualFile
{
private string content;
private QS_VirtualPathProvider spp;
public bool Exists
{
get { return (content != null); }
}
public QS_VirtualFile(string virtualPath, QS_VirtualPathProvider provider) : base(virtualPath)
{
this.spp = provider;
GetData(virtualPath);
}
public QS_VirtualFile(QDynamicView vw, string virtualPath) : base(virtualPath)
{
content = vw.ViewCode;
}
private byte[] _BinaryContent;
public QS_VirtualFile(string virtualPath, byte[] contents) : base(virtualPath)
{
this._BinaryContent = contents;
}
protected void GetData(string virtualPath)
{
QDynamicView QSView = spp.GetVirtualData(virtualPath);
if (QSView != null)
{
content = QSView.ViewCode;
}
}
public override Stream Open()
{
return new MemoryStream(_BinaryContent);
}
}
Step 3: register the controller factory and the virtual path provider in the in Global.asax** file:
HostingEnvironment.RegisterVirtualPathProvider(new QS_VirtualPathProvider());
ControllerBuilder.Current.SetControllerFactory(new QS_DynamicControllerFactory());
testing the code
in order to test the code above i added a controller named (test) and a view named (testView.cshtml) in the database and requested the url below:
http://localhost:1001/qzone/test/TestView
and I got this error
I guess this mean that the controller factory worked fine but the view was not loaded
Any ideas?
That's because it's looking for your view on the hard drive. The View Engine uses VirtualPathProvidersto resolve your views, so you need to write your own VirtualPathProvider and register it.
You can find the documentation here:
https://learn.microsoft.com/en-us/dotnet/api/system.web.hosting.virtualpathprovider?view=netframework-4.8
Unfortunately, it is way too much code for me to copy here, but you can find a full example there.
Mind you, the example is for .NET 4.8, so if you're using Core, this may not be applicable.

MVC 5 VirtualPathProvider not working as expected

I've created the following virtual path provider to load views from a DB and when the view doesn't exist on disk, I am seeing my DB method in be called and return true for the FileExists method. After that, no other methods are called and the page returns as a 404. Views that are on disk are still rendering fine. The DB call GetByVirtualPath just returns a views content. I have validated that this object is hydrated with data.
VirtualPathProvider
public class CMSVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
return base.FileExists(virtualPath) || MVCViewVersion.GetByVirtualPath(virtualPath) != null;
}
public override VirtualFile GetFile(string virtualPath)
{
if (base.FileExists(virtualPath))
{
return base.GetFile(virtualPath);
}
else
{
return new CMSVirtualFile(virtualPath, this);
}
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
if (base.FileExists(virtualPath))
{
return base.GetFileHash(virtualPath, virtualPathDependencies);
}
else
{
#if DEBUG
return null;
#else
return string.Format("{0}{1}", virtualPath, DateTime.UtcNow.ToString("dd HH"));
#endif
}
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (!base.FileExists(virtualPath))
{
return null;
}
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
VirtualFile
This class is never hit.
public class CMSVirtualFile : VirtualFile
{
private CMSVirtualPathProvider _ParentProvider;
public CMSVirtualFile(string virtualPath, CMSVirtualPathProvider parentProvider)
: base(virtualPath)
{
_ParentProvider = parentProvider;
}
public override System.IO.Stream Open()
{
string Content = string.Empty;
MVCViewVersion Version = MVCViewVersion.GetByVirtualPath(this.VirtualPath);
if (Version != null)
{
Content = Version.Content;
}
return new MemoryStream(ASCIIEncoding.Default.GetBytes(Content));
}
}
In the global.asax, I added the following link in the Application_Start method.
HostingEnvironment.RegisterVirtualPathProvider(new CMSVirtualPathProvider());
I assume something has changed as this code works on my previous implementation for MVC4. I can't put my finger on what I am doing incorrectly.
Found the issue. This was interesting. When the FileExists check occurs the virtualPath is passed like this:
~/Views/Home/Index.cshml
When the GetFile is called the virtualPath is:
/Views/Home/Index.cshtml
This causes the DB query to try and pull using the wrong virtual path from the DB which returns a null value. This then throws a 404. What a simple fix for a nightmare to find problem.

Including a MVC View or string in a javascript bundle using Bundling framework

I'm trying to bundle several javascript files together, but I also need to include a variable from app.config in the js.
My thought was to use a Controller to return a string to set the variable, so going to
~/Javascript/Index would return var foo = "bar"; This works fine.
But when I try to build a bundle, the static files are being included, but the string (or view) isn't showing up. In looking around I found that the Optimizing framework was limited to static files up until 1.1 when support for VirtualPathProviders was implemented.
I upgraded to the latest package, but I can't find any information on how to get a mix of static files and ones generated by a Controller/View to bundle. I guess I just want to use the MVC path provider?
My other thought was to try to use the bundling engine to build a string of the bundled static files and then just append my string and return it all to the browser. But, I can't find a method that allows me to use the bundling engine to return a result of the bundling process.
Integration of dynamic content into the bundling process requires the following steps:
Writing the logic that requests / builds the required content. Generating content from Controller directly requires a bit of work:
public static class ControllerActionHelper
{
public static string RenderControllerActionToString(string virtualPath)
{
HttpContext httpContext = CreateHttpContext(virtualPath);
HttpContextWrapper httpContextWrapper = new HttpContextWrapper(httpContext);
RequestContext httpResponse = new RequestContext()
{
HttpContext = httpContextWrapper,
RouteData = RouteTable.Routes.GetRouteData(httpContextWrapper)
};
// Set HttpContext.Current if RenderActionToString is called outside of a request
if (HttpContext.Current == null)
{
HttpContext.Current = httpContext;
}
IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
IController controller = controllerFactory.CreateController(httpResponse,
httpResponse.RouteData.GetRequiredString("controller"));
controller.Execute(httpResponse);
return httpResponse.HttpContext.Response.Output.ToString();
}
private static HttpContext CreateHttpContext(string virtualPath)
{
HttpRequest httpRequest = new HttpRequest(string.Empty, ToDummyAbsoluteUrl(virtualPath), string.Empty);
HttpResponse httpResponse = new HttpResponse(new StringWriter());
return new HttpContext(httpRequest, httpResponse);
}
private static string ToDummyAbsoluteUrl(string virtualPath)
{
return string.Format("http://dummy.net{0}", VirtualPathUtility.ToAbsolute(virtualPath));
}
}
Implement a virtual path provider that wraps the existing one and intercept all virtual paths that should deliver the dynamic content.
public class ControllerActionVirtualPathProvider : VirtualPathProvider
{
public ControllerActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
{
// Wrap an existing virtual path provider
VirtualPathProvider = virtualPathProvider;
}
protected VirtualPathProvider VirtualPathProvider { get; set; }
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
}
public override bool DirectoryExists(string virtualDir)
{
return VirtualPathProvider.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return true;
}
return VirtualPathProvider.FileExists(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies,
DateTime utcStart)
{
AggregateCacheDependency aggregateCacheDependency = new AggregateCacheDependency();
List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
// Create CacheDependencies for our virtual Controller Action paths
foreach (string virtualPathDependency in virtualPathDependenciesCopy.ToList())
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPathDependency))
{
aggregateCacheDependency.Add(new ControllerActionCacheDependency(virtualPathDependency));
virtualPathDependenciesCopy.Remove(virtualPathDependency);
}
}
// Aggregate them with the base cache dependency for virtual file paths
aggregateCacheDependency.Add(VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy,
utcStart));
return aggregateCacheDependency;
}
public override string GetCacheKey(string virtualPath)
{
return VirtualPathProvider.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return VirtualPathProvider.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return new ControllerActionVirtualFile(virtualPath,
new MemoryStream(Encoding.Default.GetBytes(ControllerActionHelper.RenderControllerActionToString(virtualPath))));
}
return VirtualPathProvider.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
}
public override object InitializeLifetimeService()
{
return VirtualPathProvider.InitializeLifetimeService();
}
}
public class ControllerActionVirtualFile : VirtualFile
{
public CustomVirtualFile (string virtualPath, Stream stream)
: base(virtualPath)
{
Stream = stream;
}
public Stream Stream { get; private set; }
public override Stream Open()
{
return Stream;
}
}
You also have to implement CacheDependency if you need it:
public class ControllerActionCacheDependency : CacheDependency
{
public ControllerActionCacheDependency(string virtualPath, int actualizationTime = 10000)
{
VirtualPath = virtualPath;
LastContent = GetContentFromControllerAction();
Timer = new Timer(CheckDependencyCallback, this, actualizationTime, actualizationTime);
}
private string LastContent { get; set; }
private Timer Timer { get; set; }
private string VirtualPath { get; set; }
protected override void DependencyDispose()
{
if (Timer != null)
{
Timer.Dispose();
}
base.DependencyDispose();
}
private void CheckDependencyCallback(object sender)
{
if (Monitor.TryEnter(Timer))
{
try
{
string contentFromAction = GetContentFromControllerAction();
if (contentFromAction != LastContent)
{
LastContent = contentFromAction;
NotifyDependencyChanged(sender, EventArgs.Empty);
}
}
finally
{
Monitor.Exit(Timer);
}
}
}
private string GetContentFromControllerAction()
{
return ControllerActionHelper.RenderControllerActionToString(VirtualPath);
}
}
Register your virtual path provider:
public static void RegisterBundles(BundleCollection bundles)
{
// Set the virtual path provider
BundleTable.VirtualPathProvider = new ControllerActionVirtualPathProvider(BundleTable.VirtualPathProvider);
bundles.Add(new Bundle("~/bundle")
.Include("~/Content/static.js")
.Include("~/JavaScript/Route1")
.Include("~/JavaScript/Route2"));
}
Optional: Add Intellisense support to your views. Use <script> tags within your View and let them be removed by a custom ViewResult:
public class DynamicContentViewResult : ViewResult
{
public DynamicContentViewResult()
{
StripTags = false;
}
public string ContentType { get; set; }
public bool StripTags { get; set; }
public string TagName { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null)
{
result = FindView(context);
View = result.View;
}
string viewResult;
using (StringWriter viewContentWriter = new StringWriter())
{
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, viewContentWriter);
View.Render(viewContext, viewContentWriter);
if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
viewResult = viewContentWriter.ToString();
// Strip Tags
if (StripTags)
{
string regex = string.Format("<{0}[^>]*>(.*?)</{0}>", TagName);
Match res = Regex.Match(viewResult, regex,
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Singleline);
if (res.Success && res.Groups.Count > 1)
{
viewResult = res.Groups[1].Value;
}
else
{
throw new InvalidProgramException(
string.Format("Dynamic content produced by View '{0}' expected to be wrapped in '{1}' tag.", ViewName, TagName));
}
}
}
context.HttpContext.Response.ContentType = ContentType;
context.HttpContext.Response.Output.Write(viewResult);
}
}
Use an extension method or add an helper function to your controller:
public static DynamicContentViewResult JavaScriptView(this Controller controller, string viewName, string masterName, object model)
{
if (model != null)
{
controller.ViewData.Model = model;
}
return new DynamicContentViewResult
{
ViewName = viewName,
MasterName = masterName,
ViewData = controller.ViewData,
TempData = controller.TempData,
ViewEngineCollection = controller.ViewEngineCollection,
ContentType = "text/javascript",
TagName = "script",
StripTags = true
};
}
The steps are similiar for other type of dynamic contents. See Bundling and Minification and Embedded Resources for example.
I added a proof of concept repository to GitHub if you want to try it out.

ASP.NET bundling/minification: including dynamically generated Javascript

I have a site that dynamically generates Javascript. The generated code describes type-metadata and some server-side constants so that the clients can easily consume the server's services - so it's very cacheable.
The generated Javascript is served by an ASP.NET MVC controller; so it has a Uri; say ~/MyGeneratedJs.
I'd like to include this Javascript in a Javascript bundle with other static Javascript files (e.g. jQuery etc): so just like static files I want it to be referenced separately in debug mode and in minified form bundled with the other files in non-debug mode.
How can I include dynamically generated Javascript in a bundle?
With VirtualPathProviders this is now possible. Integration of dynamic content into the bundling process requires the following steps:
Writing the logic that requests / builds the required content. Generating content from Controller directly requires a bit of work:
public static class ControllerActionHelper
{
public static string RenderControllerActionToString(string virtualPath)
{
HttpContext httpContext = CreateHttpContext(virtualPath);
HttpContextWrapper httpContextWrapper = new HttpContextWrapper(httpContext);
RequestContext httpResponse = new RequestContext()
{
HttpContext = httpContextWrapper,
RouteData = RouteTable.Routes.GetRouteData(httpContextWrapper)
};
// Set HttpContext.Current if RenderActionToString is called outside of a request
if (HttpContext.Current == null)
{
HttpContext.Current = httpContext;
}
IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
IController controller = controllerFactory.CreateController(httpResponse,
httpResponse.RouteData.GetRequiredString("controller"));
controller.Execute(httpResponse);
return httpResponse.HttpContext.Response.Output.ToString();
}
private static HttpContext CreateHttpContext(string virtualPath)
{
HttpRequest httpRequest = new HttpRequest(string.Empty, ToDummyAbsoluteUrl(virtualPath), string.Empty);
HttpResponse httpResponse = new HttpResponse(new StringWriter());
return new HttpContext(httpRequest, httpResponse);
}
private static string ToDummyAbsoluteUrl(string virtualPath)
{
return string.Format("http://dummy.net{0}", VirtualPathUtility.ToAbsolute(virtualPath));
}
}
Implement a virtual path provider that wraps the existing one and intercept all virtual paths that should deliver the dynamic content.
public class ControllerActionVirtualPathProvider : VirtualPathProvider
{
public ControllerActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
{
// Wrap an existing virtual path provider
VirtualPathProvider = virtualPathProvider;
}
protected VirtualPathProvider VirtualPathProvider { get; set; }
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
}
public override bool DirectoryExists(string virtualDir)
{
return VirtualPathProvider.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return true;
}
return VirtualPathProvider.FileExists(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies,
DateTime utcStart)
{
AggregateCacheDependency aggregateCacheDependency = new AggregateCacheDependency();
List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
// Create CacheDependencies for our virtual Controller Action paths
foreach (string virtualPathDependency in virtualPathDependenciesCopy.ToList())
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPathDependency))
{
aggregateCacheDependency.Add(new ControllerActionCacheDependency(virtualPathDependency));
virtualPathDependenciesCopy.Remove(virtualPathDependency);
}
}
// Aggregate them with the base cache dependency for virtual file paths
aggregateCacheDependency.Add(VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy,
utcStart));
return aggregateCacheDependency;
}
public override string GetCacheKey(string virtualPath)
{
return VirtualPathProvider.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return VirtualPathProvider.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return new ControllerActionVirtualFile(virtualPath,
new MemoryStream(Encoding.Default.GetBytes(ControllerActionHelper.RenderControllerActionToString(virtualPath))));
}
return VirtualPathProvider.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
}
public override object InitializeLifetimeService()
{
return VirtualPathProvider.InitializeLifetimeService();
}
}
public class ControllerActionVirtualFile : VirtualFile
{
public CustomVirtualFile (string virtualPath, Stream stream)
: base(virtualPath)
{
Stream = stream;
}
public Stream Stream { get; private set; }
public override Stream Open()
{
return Stream;
}
}
You also have to implement CacheDependency if you need it:
public class ControllerActionCacheDependency : CacheDependency
{
public ControllerActionCacheDependency(string virtualPath, int actualizationTime = 10000)
{
VirtualPath = virtualPath;
LastContent = GetContentFromControllerAction();
Timer = new Timer(CheckDependencyCallback, this, actualizationTime, actualizationTime);
}
private string LastContent { get; set; }
private Timer Timer { get; set; }
private string VirtualPath { get; set; }
protected override void DependencyDispose()
{
if (Timer != null)
{
Timer.Dispose();
}
base.DependencyDispose();
}
private void CheckDependencyCallback(object sender)
{
if (Monitor.TryEnter(Timer))
{
try
{
string contentFromAction = GetContentFromControllerAction();
if (contentFromAction != LastContent)
{
LastContent = contentFromAction;
NotifyDependencyChanged(sender, EventArgs.Empty);
}
}
finally
{
Monitor.Exit(Timer);
}
}
}
private string GetContentFromControllerAction()
{
return ControllerActionHelper.RenderControllerActionToString(VirtualPath);
}
}
Register your virtual path provider:
public static void RegisterBundles(BundleCollection bundles)
{
// Set the virtual path provider
BundleTable.VirtualPathProvider = new ControllerActionVirtualPathProvider(BundleTable.VirtualPathProvider);
bundles.Add(new Bundle("~/bundle")
.Include("~/Content/static.js")
.Include("~/JavaScript/Route1")
.Include("~/JavaScript/Route2"));
}
Optional: Add Intellisense support to your views. Use <script> tags within your View and let them be removed by a custom ViewResult:
public class DynamicContentViewResult : ViewResult
{
public DynamicContentViewResult()
{
StripTags = false;
}
public string ContentType { get; set; }
public bool StripTags { get; set; }
public string TagName { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null)
{
result = FindView(context);
View = result.View;
}
string viewResult;
using (StringWriter viewContentWriter = new StringWriter())
{
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, viewContentWriter);
View.Render(viewContext, viewContentWriter);
if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
viewResult = viewContentWriter.ToString();
// Strip Tags
if (StripTags)
{
string regex = string.Format("<{0}[^>]*>(.*?)</{0}>", TagName);
Match res = Regex.Match(viewResult, regex,
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Singleline);
if (res.Success && res.Groups.Count > 1)
{
viewResult = res.Groups[1].Value;
}
else
{
throw new InvalidProgramException(
string.Format("Dynamic content produced by View '{0}' expected to be wrapped in '{1}' tag.", ViewName, TagName));
}
}
}
context.HttpContext.Response.ContentType = ContentType;
context.HttpContext.Response.Output.Write(viewResult);
}
}
Use an extension method or add an helper function to your controller:
public static DynamicContentViewResult JavaScriptView(this Controller controller, string viewName, string masterName, object model)
{
if (model != null)
{
controller.ViewData.Model = model;
}
return new DynamicContentViewResult
{
ViewName = viewName,
MasterName = masterName,
ViewData = controller.ViewData,
TempData = controller.TempData,
ViewEngineCollection = controller.ViewEngineCollection,
ContentType = "text/javascript",
TagName = "script",
StripTags = true
};
}
The steps are similiar for other type of dynamic contents. See Bundling and Minification and Embedded Resources for example.
I added a proof of concept repository to GitHub if you want to try it out.
Darin is right, currently bundling only works on static files. But if you can add a placeholder file with up to date content, bundling does setup file change notifications which will detect automatically when the placeholder file changes.
Also we are going to be moving to using VirtualPathProviders soon which might be a way to serve dynamically generated content.
Update: The 1.1-alpha1 release is out now which has support for VPP
This is not possible. Bundles work only with static files.

Embedded razor views

Recently, I read a post where the author describes how we can compile razor views into separate libraries. I would like to ask, is it possible to embed views in libraries without compiling? And then, add custom VirtualPathProvider to read the views.
You can use my EmbeddedResourceVirtualPathProvider which can be installed via Nuget. It loads resources from referenced assemblies, and also can be set to take dependencies on the source files during development so you can update views without needing a recompile.
In your "shell" MVC project's Global.asax Application_Start register your custom VirtualPathProvider:
HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider());
The actual implementation would be more complex than this because you would likely do some interface-based, reflection, database lookup, etc as a means of pulling metadata, but this would be the general idea (assume you have another MVC project named "AnotherMvcAssembly" with a Foo controller and the Index.cshtml View is marked as an embedded resource:
public class CustomVirtualPathProvider : VirtualPathProvider {
public override bool DirectoryExists(string virtualDir) {
return base.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath) {
if (virtualPath == "/Views/Foo/Index.cshtml") {
return true;
}
else {
return base.FileExists(virtualPath);
}
}
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart) {
if (virtualPath == "/Views/Foo/Index.cshtml") {
Assembly asm = Assembly.Load("AnotherMvcAssembly");
return new CacheDependency(asm.Location);
}
else {
return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
public override string GetCacheKey(string virtualPath) {
return base.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir) {
return base.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath) {
if (virtualPath == "/Views/Foo/Index.cshtml") {
return new CustomVirtualFile(virtualPath);
}
else {
return base.GetFile(virtualPath);
}
}
}
public class CustomVirtualFile : VirtualFile {
public CustomVirtualFile(string virtualPath) : base(virtualPath) { }
public override System.IO.Stream Open() {
Assembly asm = Assembly.Load("AnotherMvcAssembly");
return asm.GetManifestResourceStream("AnotherMvcAssembly.Views.Foo.Index.cshtml");
}
}

Resources