Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 385
Eamon Nerbonne Avatar asked Aug 18 '12 12:08

Eamon Nerbonne


1 Answers

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.

like image 52
Dresel Avatar answered Sep 22 '22 19:09

Dresel