10

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?

Michael Gaskill
  • 7,913
  • 10
  • 38
  • 43
Eamon Nerbonne
  • 47,023
  • 20
  • 101
  • 166

3 Answers3

7

With VirtualPathProviders this is now possible. Integration of dynamic content into the bundling process requires the following steps:

  1. 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));
        }
    }
    
  2. 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);
        }
    }
    
  3. 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"));
    }
    
  4. 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.

Peter
  • 37,042
  • 39
  • 142
  • 198
Dresel
  • 2,375
  • 1
  • 28
  • 44
4

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

Hao Kung
  • 28,040
  • 6
  • 84
  • 93
  • That sounds good! Do you have a blog or some news site that you'd recommend to follow for these kind of developments? – Eamon Nerbonne Aug 20 '12 at 23:44
  • We just made our codeplex site public, the code isn't there yet, but this is probably what you want long term: http://aspnetoptimization.codeplex.com – Hao Kung Aug 29 '12 at 18:42
  • 1
    don't really understand what you mean by placeholder file. If I have a file on disk with the same path as my dynamic mvc route, it will be returned and my action will never get executed. What am I missing? – Paul Hiles Oct 13 '12 at 00:03
  • also any update on the VirtualPathProviders change? Can't see any code on the Codeplex site. – Paul Hiles Oct 13 '12 at 00:04
  • The VPP change will be in the 1.1-alpha1 release which should be out shortly – Hao Kung Oct 15 '12 at 20:25
  • The 1.1-alpha1 release is out now: http://nuget.org/packages/Microsoft.AspNet.Web.Optimization – Hao Kung Oct 19 '12 at 23:53
3

This is not possible. Bundles work only with static files.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • I'm OK with adding a placeholder file if necessary - I just want the content to automatically be in sync with the server-side code. – Eamon Nerbonne Aug 18 '12 at 16:16