Marius Schulz
Marius Schulz
Front End Engineer

Generating External JavaScript Files Using Partial Razor Views

Just for the record, I love ASP.NET MVC. It truly is a great framework for web development, and over the years it has served — and continues to serve — me very well. That said, I sometimes stumble upon problems for which I think the framework should have a built-in solution.

Just recently, I came across one of those problems when I needed to pass some generated URL configuration to JavaScript without cluttering my HTML. It worked out nicely in the end, but not without extending ASP.NET MVC with a custom action filter attribute. I feel like I've found a clean way to solve that problem, which motivated me to write this blog post and share my solution with you, so here we go.

#Breaking the Application with Absolute URLs

I don't like hardcoding absolute URLs in my JavaScript code, it's bad practice anyway. When deploying an ASP.NET application to different servers, I don't want to be forced to have to adhere to a certain application path. My application might run under e.g. https://localhost:12345 locally, but under https://example.com/demoapp in production. Note the /demoapp part in the latter URL, which makes the following jQuery AJAX call fail:

$.getJSON("/comments/latest", function (comments) {
  // ...
});

Because of the leading slash, the URL that's being queried here is https://example.com/comments/latest, which is not what I wanted it to be (notice the missing /demoapp section).

#Rendering the Application Root URL in the Layout File

The issue of absolute URLs is easily solved by letting ASP.NET MVC generate the website root URL and setting it as a global JavaScript variable in the _Layout.cshtml file:

<script>
    window.rootUrl = '@Url.Content("~/")';
</script>

This window.rootUrl property can then be used to fix the URL for our AJAX call, which now works as intended:

$.getJSON(window.rootUrl + "comments/latest", function (comments) {
  // ...
});

However, that approach has two drawbacks:

  1. The layout file gets cluttered with JavaScript configuration settings.
  2. The configuration is rendered directly in the response HTML.

While the first drawback could be avoided by encapsulating the <script> tag within a partial view or a child action, the configuration would still be rendered directly in the response HTML. Instead, I'd prefer the configuration to be contained within an external JavaScript file that can be referenced in the layout file.

#Generating an External JavaScript File for the Configuration

At that point, you might argue that's it's easy to dynamically generate files in an ASP.NET MVC controller by simply returning a view with the desired content. True, you can do that. That's how I started out my controller, too:

using System.Web.Mvc;

namespace DemoApp
{
    public class JavaScriptSettingsController : Controller
    {
        public ActionResult Index()
        {
            return PartialView();
        }
    }
}

In the corresponding Index.cshtml Razor view, I would simply have to output the configuration:

window.rootUrl = '@Url.Content("~/")';

Then I can reference the above external script in the layout file. For the sake of simplicity, I'm relying on the default route here, which is {controller}/{action}/{id}:

<script src="~/JavaScriptSettings"></script>

Are we done yet? Well, not really. While the browser is perfectly happy with the referenced JavaScript file, we aren't: Visual Studio doesn't provide us with tooling support when writing the view because it's just plain text with some embedded Razor code; the IDE doesn't know that what we wrote is meant to be executable JavaScript.

Now let me show you my pretty straightforward solution: Let's make it recognizable JavaScript.

#Wrapping the Configuration in Script Tags

This step is an easy one as we simply need to wrap our configuration in <script> tags, like that:

<script>
    window.rootUrl = '@Url.Content("~/")';
</script>

We now get all the tooling advantages that Visual Studio and ReSharper give us: IntelliSense, code analysis, refactoring support, … That sort of help surely isn't necessary for a one-liner like ours, but our JavaScript code could be much more sophisticated and complex — think about modules and not just a configuration file.

Are we done now? Again, not entirely, but we're getting close. Since external JavaScript files can't have their code wrapped in <script> tags, we broke our example by adding these tags. Hmm, not good. To make this work again, we'll have to strip the script tags from the response when delivering the partial view. Remember the custom action filter attribute that I briefly mentioned in the introductory paragraph? Now it finally comes into play.

#Removing the Script Tags with a Custom Action Filter Attribute

To get rid of the enclosing <script> and </script> tags, I wrote a custom ASP.NET MVC action filter called ExternalJavaScriptFileAttribute. It uses a regular expression to remove the script tags and also sets the appropriate content type header for the resulting JavaScript file:

public class ExternalJavaScriptFileAttribute : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        var response = filterContext.HttpContext.Response;
        response.Filter = new StripEnclosingScriptTagsFilter(response.Filter);
        response.ContentType = "text/javascript";
    }

    private class StripEnclosingScriptTagsFilter : MemoryStream
    {
        private static readonly Regex LeadingOpeningScriptTag;
        private static readonly Regex TrailingClosingScriptTag;

        private readonly StringBuilder _output;
        private readonly Stream _responseStream;

        static StripEnclosingScriptTagsFilter()
        {
            LeadingOpeningScriptTag = new Regex(@"^\s*<script[^>]*>", RegexOptions.Compiled);
            TrailingClosingScriptTag = new Regex(@"</script>\s*$", RegexOptions.Compiled);
        }

        public StripEnclosingScriptTagsFilter(Stream responseStream)
        {
            _responseStream = responseStream;
            _output = new StringBuilder();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            string response = GetStringResponse(buffer, offset, count);
            _output.Append(response);
        }

        public override void Flush()
        {
            string response = _output.ToString();

            if (LeadingOpeningScriptTag.IsMatch(response) && TrailingClosingScriptTag.IsMatch(response))
            {
                response = LeadingOpeningScriptTag.Replace(response, string.Empty);
                response = TrailingClosingScriptTag.Replace(response, string.Empty);
            }

            WriteStringResponse(response);
            _output.Clear();
        }

        private static string GetStringResponse(byte[] buffer, int offset, int count)
        {
            byte[] responseData = new byte[count];
            Buffer.BlockCopy(buffer, offset, responseData, 0, count);

            return Encoding.Default.GetString(responseData);
        }

        private void WriteStringResponse(string response)
        {
            byte[] outdata = Encoding.Default.GetBytes(response);
            _responseStream.Write(outdata, 0, outdata.GetLength(0));
        }
    }
}

If you're not big on regular expressions, don't worry. The regex matches all responses that start with an opening script tag (which can have attributes, such as type="text/javascript") and end with a closing one. The response can also have optional leading and trailing whitespace, that doesn't matter. That's it!

All that's left to do now for the magic to happen is to decorate the Index() action method of our JavaScriptSettingsController with the [ExternalJavaScriptFile] attribute:

using System.Web.Mvc;

namespace DemoApp
{
    public class JavaScriptSettingsController : Controller
    {
        [ExternalJavaScriptFile]
        public ActionResult Index()
        {
            return PartialView();
        }
    }
}

The beauty is that the action method can return whatever ActionResult you want to; the action filter doesn't care how the resulting HTML was generated, so you could also return a ContentResult, for example.

I also created a Gist for the ExternalJavaScriptFileAttribute, so feel free to fork it or submit your improvements and suggestions. With this in mind: happy coding, everyone!