I have a controller action that returns Javascript file. I can reference this file from my view and it works fine. I\'d like to put it in a System.Web.Optimization.Bundle with t
Bundling system only supports physical files rather than application routes.
I think this is quite doable - but before I get into my solution - Remember that the bundle is created on the first hit and is reused - this means that any 'dynamic' script still has to be global (ie it cannot be dependant on a specific user etc). This is why generally it only allows static js files. Saying that... I could imagine a situation where you might want to stick variables like version number or something like that in your js (though in that case I personally would just use Ajax/JSON to get it).
I think the way to go about it is to create an derived type from Bundle. In it you would overwrite the EnumerateFiles method. Initially you would enumerate over the base.EnumerateFiles and then include your own virtual file.
Something like (Note: not tested code):
public class VirtualMethodBundle : Bundle
{
private List<VirtualFile> _virtualContent = new List<VirtualFile>();
public override IEnumerable<VirtualFile> EnumerateFiles(BundleContext context)
{
foreach(var file in base.EnumerateFiles(context))
{
yield return file;
}
foreach(var virtual in _virtualContent)
{
yield return virtual;
}
}
public void AddCustomFile(VirtualFile file)
{
_virtualContent.Add(method);
}
}
Then you would have a special VirtualFile defined type that overrides the Open/Name method and returns your dynamic content there... Something like.
public class MethodBasedVirtualFile : VirtualFile
{
private readonly Func<string> _contentFactory;
private readonly string _path;
public MethodBasedVirtualFile(string path, Func<string> contentFactory)
{
_path = path;
_contentFactory = contentFactory;
}
public override string Name { get { return _path; } }
public override Stream Open()
{
MemoryStream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write(_contentFactory());
writer.Flush();
stream.Position = 0;
return stream;
}
}
So then to use it all you would have...
var bundle = new VirtualMethodBundle();
bundle.Include(... real files ...);
bundle.AddCustomFile(
new MethodBasedVirtualFile("~/DynamicScript/UrlDictionary",
... the method that creates the content of that script...)
);
If you were clever you could just make a UrlVirtualFile that takes the url path and uses MVC to automatically get the content when required.
Here is what I did for this scenario. I defined a "Bundle Result" and then in my Controller method, just returned a Bundle by name. I am using ETags (If-None-Match Header) as an optimization for client caching.
eg:
public ActionResult ReturnFooBundle()
{
return new BundleResult("foo", TimeSpan.FromDays(7));
}
Here is the BundleResult implementation:
public class BundleResult
: ActionResult
{
private class BundleInfo
{
public string BundleETag;
public DateTime LastModified;
public Bundle TheBundle;
public BundleResponse Response;
}
private static Dictionary<string, BundleInfo> _bundleCache = new Dictionary<string, BundleInfo>();
public string BundleName { get; private set; }
public TimeSpan CacheExpiry { get; private set; }
public BundleResult(string bundleName, TimeSpan cacheExpiry)
{
BundleName = bundleName;
CacheExpiry = cacheExpiry;
}
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.Clear();
BundleInfo bundleInfo = GetBundle(context.HttpContext);
string requestETag = context.HttpContext.Request.Headers["If-None-Match"];
if (!string.IsNullOrEmpty(requestETag) && (requestETag == bundleInfo.BundleETag))
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified;
context.HttpContext.Response.StatusDescription = "Not Modified";
return;
}
else
{
BundleResponse bundleResponse = bundleInfo.Response;
HttpResponseBase response = context.HttpContext.Response;
response.Write(bundleResponse.Content);
response.ContentType = bundleResponse.ContentType;
HttpCachePolicyBase cache = response.Cache;
cache.SetCacheability(HttpCacheability.ServerAndPrivate);
cache.SetLastModified(bundleInfo.LastModified);
cache.SetETag(bundleInfo.BundleETag);
}
}
private BundleInfo GetBundle(HttpContextBase context)
{
// lookup the BundleResponse
BundleInfo retVal;
lock (_bundleCache)
{
_bundleCache.TryGetValue(BundleName, out retVal);
}
if(retVal != null)
{
#if DEBUG
// see if the contents have been modified.
BundleContext bundleContext = new BundleContext(context, BundleTable.Bundles, BundleName);
DateTime lastModified = retVal.TheBundle.EnumerateFiles(bundleContext).Select(fi => fi.LastWriteTimeUtc).Max();
if (lastModified > retVal.LastModified)
{
// regenerate the bundleInfo
retVal = null;
}
#endif
}
if (retVal == null)
{
string rawBundleName = BundleTable.Bundles.ResolveBundleUrl(BundleName);
string hash = rawBundleName.Substring(rawBundleName.IndexOf("?v=") + 3);
Bundle bundle = BundleTable.Bundles.GetBundleFor(BundleName);
BundleContext bundleContext = new BundleContext(context, BundleTable.Bundles, BundleName);
BundleResponse bundleResponse = bundle.GenerateBundleResponse(bundleContext);
DateTime lastModified = bundle.EnumerateFiles(bundleContext).Select(fi => fi.LastWriteTimeUtc).Max();
retVal = new BundleInfo
{
BundleETag = hash,
Response = bundleResponse,
TheBundle = bundle,
LastModified = lastModified,
};
lock (_bundleCache)
{
_bundleCache[BundleName] = retVal;
}
}
return retVal;
}
}