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. http://localhost:12345 locally, but under http://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 http://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!

Use the coupon code LAUNCHDAY for $10 off!

Learn ES6

44 Comments

Lennox M

Is it still possible to add your external JavaScript file to a bundle and minify it?

John Reilly

Interesting post that tackles something I've been dealing with recently as well. We tackled it by having the following meta tag in our _Layout.cshtml:

<meta name="root" content="@Request.ApplicationPath" />

And already had a bootstrap JS file that is served up on each page that does things like initialise menus etc. We included in that the following JavaScript (which is exposed using the the revealing module pattern):

// Extract the application root from the "root" meta tag and ensure ends with "/"
rootUrl = $('meta[name="root"]').prop("content");
if (rootUrl.slice(-1) !== "/") {
    rootUrl += "/";
}

Interesting to see a different approach. Maybe we'll switch to it at some point...

Joe

Interesting approach; I've often wondered the best way to do this whilst keeping the HTML clean of Javascript. John's method sounds like a good option too.

Peter Munnings

How does debugging work in this environment. Is it easy enough to debug through the javascript?

Marius Schulz

Common bundling & minification libraries, such as Combres or System.Web.Optimization, work directly with the file system. This means they're loading the file directly from disk; they're not making an HTTP Request to the action method that I describe in this post. The razor code thus won't be parsed.

tl;dr: No, you can't simply bundle and minify this settings file as is.

Marius Schulz

I've done similar things by decorating an invisible dummy HTML element with data- attributes which I would then read using jQuery's $.data() method.

However, once the configuration reaches a certain complexity or simply gets long, I definitely prefer this (also much cleaner) approach. You're right, though — it's probably overkill for a one-liner.

Marius Schulz

Debugging is trivial: Since the action method returns a plain JavaScript file that can be parsed by the browser, you can simply make a call to <span class="monospace">/JavaScriptSettings</span> and study the output!

Marius Schulz

For simple string values, John's solution might be more appropriate; it certainly is the simpler one. As soon as you need actual JavaScript objects and not just numbers or strings, using <meta> tags can get messy pretty quickly. The only valid answer here is it depends.

nick

I have a question. Would you then be able to send tempData a javascript object to maintain namespace scope in different pages?

Marius Schulz

I'm not exactly sure I understand your question correctly, but if you're asking if you can use TempData or ViewData within the Razor view, then yes, you can. The pipeline is executed as usual which means you can take advantage of all the MVC goodness; only the sending the HTML response to the client is intercepted to strip out the <script> tags.

Jacob Wakeem

Nice and clever approach. There might be one problem in this solution, and that the write method of the filter could be called several time, with a a chunk of the response in each time.(depending on how large your view is). So there is a risk that non of these chunks contains the opening or the closing script tag.

Marius Schulz

The OnResultExecuted is called just after the result is executed and it's only called once, so there shouldn't be a problem if I'm not missing something here.

Jacob Wakeem

The OnResultExecuted is called once. But the Write function of the StripEnclosingScriptTagsFilter is not guaranteed to be called only once as the asp.net runtime sends the output to the client in chunks. If you have a big javascript file to send, there might be a problem.

Here is a good explanation: Capturing and Transforming ASP.NET Output with Response.Filter

Mandeep

This is an awesome approach, especially, when we have to store lots of variables, related to Culture string resources, and other stuff coming from .net app.

Thanks.

Jon Davis

Truthfully I don't like the solution. By the time you created a custom ExternalJavaScriptFileAttribute class that you now have to support as several lines of compiled code, you open the door to a number of other options.

For example, you could put your AJAX Javascript in a special nested directory with an HTTP filter that, when its contents are accessed, all instances of "~/" are dynamically replaced with the application root path. Then just use

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

.. etc.

Personally I had always been completely in favor of "cluttering up" the master layout file with obvious bindings like window.rootUrl = '@Url.Content("~/")' because these are NOT configuration settings. Configuration settings are changeable settings. What you have here is a binding of application root to a Javascript variable, and that does not change (it should be "configured" only in IIS Manager when the application is set up as a virtual directory). So if that's clutter then so is "<html>" because gosh "<html>" is a configuration setting, it could be "<zjfier>". (kidding.)

I would say that such a binding is a necessary evil of markup because there is nothing inherently built-in in web semantics to infer such a binding, but to be quite honest I think John Reilly's solution points at such a built-in solution and pattern. The <meta name="root"> tag is in fact exactly the built-in implementation that the web community implemented for us to avoid this concern.

Jon Davis

Just a side note, not sure how many people know this, but <meta name="root" content="/path/" /> is actually a special tag--not <meta> itself, but specifically <meta name="root">. The DOM parser and engine completely changes how all referenced resources including hyperlinks and image tags resolve their paths. It is to the DOM what the "Current Directory" variable is in a console application.

Marius Schulz

When it comes to a simple root URL setting as shown above, I see your point. I'm still not a fan of cluttering up my HTML directly, but that's just personal preference. The solution <meta name="root" /> is fine for that use case.

However, once you want to do a little more on the server side than just outputting a URL, meta tags won't get you far. I'm thinking about serializing complex objects to JSON. Also, when the external script contains more JavaScript logic, rendering it inline (in the layout HTML) is far from ideal, in my opinion.

My solution certainly isn't a golden hammer, but neither is any other one. In the end, it always depends on the specific use case.

Marius Schulz

This is an interesting aspect, thanks for pointing that out. How would you modify the Write method to play along with chunked responses?

Jacob Wakeem
  1. Cache the buffers that passed to the write method.
  2. Override the Flush method
  3. Move the current logic of the write method to the Flush method.

Hope it's helpful

Simon

Interesting post. I always felt the same with cluttering my HTML, but I didn't go that far. I created a partial view with all needed JavaScript settings and variables and then just used @Html.Partial("_JavaScriptSettings"). I can't really see a big advantage of your approach, to me it seem overblown. What do you think?

Marius Schulz

Simon — By using a partial view like you do, you separate the JavaScript code from your Razor view code during development time. At runtime, though, your JavaScript code is being directly included into the page's HTML.

If you don't mind that inclusion, then stick with your pragmatic approach, it's perfectly fine! My approach goes a little further and aims to keep view data (HTML) and logic (JavaScript) separate even within the browser.

Dror

Thank you for your idea and for the code.

I had to make two small changes in StripEnclosingScriptTagsFilter.Write to get the code working for me:

  1. Encoding.UTF8.GetString(buffer, offset, count) has to receive offset and count. This is important when the js file is large (mine is 10KB) and is sent in chunks of about 1KB each. The buffer contains garbage and "\0"s

  2. Replaced the Regex with a simple Replace.


public override void Write(byte[] buffer, int offset, int count)
{
    string response = Encoding.UTF8.GetString(buffer, offset, count);

    //response = _stripScriptTagsRegex.Replace(response, GetScriptTagContent);
    response = response.Replace("<script>", "").Replace("</script>", "");

    byte[] outdata = Encoding.UTF8.GetBytes(response);
    _responseStream.Write(outdata, 0, outdata.Length);
}
Rahul

Couldn't you just add "// " before each of your script opening and closing tags or surround them with "/" and "/"?

Marius Schulz

Rahul: You could wrap the <script> tags within parentheses, of course. However, why not just remove them if you don't need them anyway? ;)

Si

Hi Marius,

Thanks for the interesting article - I had this problem today, when moving a local site from the root default website and into a virtual directory.

Instead of working relative to application root, I've decided to make all URLs within my external script file relative to the bundled script file itself. So for example, I have "~/scripts/min" as my bundle's virtual path, and within my script I start my URLs with "./" rather than "/" - so this goes up a level (to my application's root level).

I hope this helps others!

sulman Qureshi

Nice Approach

Mauri Galvez

Hello there! Would I be able to use razor code within a Javascript file with this approach? I know that if I try using razor tags inside of a js file it throws an error. What I am intending to do is to populate a Model with data from a Database and then pass it to my Js file since all the functions that work with that data are in the Js file. Is it possible to accomplish this through this approach?

Thanks,

Marius Schulz

Mauri — Yes, you'd be able to use Razor code within the external JavaScript file. This is exactly what the described approach is supposed to make possible.

Yogan Rameg

This doesn't seems to working with MVC5 with VS 2013. The buffer coming as empty if you run it in VS 2013

CodingJoe

After having tried various ways and searched online to fix the url action issue from an external JS file, finally landed here. The window.rooturl method helped me.

Thanks

Ricardo Maroquio

There is a problem running it in VS 2013. In the end of the overriden Write method, the last line:

_responseStream.Write(outdata, 0, outdata.Length);

Must be replaced by:

_responseStream.Write(outdata, 0, count-17);

The count parameter is the correct size of the response output. Otherwise, lots of garbage chars will be rendered and your script will not work. You still have to subtract 17, because its the size of the removed <script></script> string. That's it. Thanks for your post, Marius!

DjangoMan

Thanks for the solution. One problem I'm facing right now: When I try to render the contents of such a JavScript Razor view in another view using Html.Action("action", "controller"), I get a "filtering is not allowed" exception. To clear things, I'm trying to create a "javascript:(//code goes here)" link. Any solutions for this? Thanks :)

Marius Schulz

@DjangoMan: The outlined approach works for full requests only. You can't use Html.Action to render the resulting JavaScript as a child action because those don't support response filters.

Omer

Just different approach It is exactly what i need. Thank you

Zootius

A couple of problems with this, that are fixed thusly:

  1. GetStringResponse() should return a string from the responseData byte array, not the buffer. Otherwise you'll get garbage bytes at the end of the output. i.e. change it to:

    return Encoding.Default.GetString(responseData);

  2. The RegEx needs to work with multiline, hence try:

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

Marius Schulz

@Zootius: Thanks for your suggestions!

  1. I've updated the GetStringResponse method accordingly. It now creates the string from responseData rather than buffer.

  2. The regular expressions are constructed to deal with the simple case of a single <script> tag within the external JavaScript file. Thus, the HTML tags only need to be stripped at the very beginning and the end of the file. RegexOptions.Multiline changes that behavior so that ^ and $ match the beginning and end of each line, which is not desired here. Check out my post on the various RegexOptions flags for more information.

stba

I had to remove the / (slash) from <script src="/JavaScriptSettings"></script> to get it to work on a http://url/folder structure

stba

But this is not working when I am in root... How to fix this?

stba

<script src="~/JavaScriptSettings"></script> seems to work

jros

Is strange but not working with Visual Studio 2015, work fine in VS2013 ...

jros

I have to clear "\0" chars and add "\n" to TrailingClosingScriptTag regex matchs ...

Machiato

This is nice. its a golteb !