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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With