Javascript controller actions

In your ASP.NET MVC application, you may need to reference action URLs, for things like AJAX calls. You can do this inline in CSHTML with something like:

var url = "@Url.Action("SavePerson", "People")";

But it’s a little harder to do it in standalone javascript files.

You can define your app root and then concatenate, like:

<script>
window._rootUrl = "@Url.Content("~")";
</script>
// In standalone .js file:
var url = _rootUrl + "/People/SavePerson";

But that only works if you’re using standard URL routing. And if you try to do it with absolute references, like /People/SavePerson, you might run into issues if your development, test, and production environments are at different levels in IIS.

One solution is to define all your routes in javascript, so you can use them globally. You can use something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Mvc;
using Newtonsoft.Json;

namespace Acme.Web {
    /// <summary>
    /// You must initialize this in order to use it. 
    /// Generally you'll want to initialize in your Global.asax.cs
    /// Application_Start method, by calling:    
    /// ActionListController.Initialize(typeof(HomeController).Assembly);
    /// If you have controllers in multiple assemblies, 
    /// pass in an array of assemblies to the Initialize method.
    /// </summary>
    public class ActionListController : Controller {
        private static Assembly[] _controllerAssemblies = new Assembly[0];

        public static void Initialize(params Assembly[] controllerAssemblies) {
            if (controllerAssemblies != null) {
                _controllerAssemblies = controllerAssemblies;
            }
        }

        [HttpGet]
        [OutputCache(Duration = 6000, VaryByParam = "none")]
        public ActionResult ControllerActionList() {
            var rootObjects = new Dictionary<string, object>();

            // Area -> Controller -> Action -> Url
            var areas = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>();

            // Controller -> Action -> Url
            var rootMethods = new Dictionary<string, Dictionary<string, string>>();

            foreach (var assembly in _controllerAssemblies) {
                foreach (var controller in assembly.GetTypes().Where(t => typeof (Controller).IsAssignableFrom(t))) {
                    string controllerName = Regex.Replace(controller.Name, "Controller$", string.Empty);
                    var areaNameMatch = Regex.Match(controller.Namespace, @"(?<=\.Areas\.)[a-zA-Z0-9_]+");
                    if (areaNameMatch.Success) {
                        if (!areas.ContainsKey(areaNameMatch.Value)) {
                            areas[areaNameMatch.Value] = new Dictionary<string, Dictionary<string, string>>();
                        }
                        areas[areaNameMatch.Value].Add(controllerName, GetActionsFromController(controller, new {Area = areaNameMatch.Value}));
                    } else {
                        rootMethods.Add(controllerName, GetActionsFromController(controller));
                    }
                }
            }
            foreach (var kvp in areas) {
                rootObjects[kvp.Key] = kvp.Value;
            }
            foreach (var kvp in rootMethods) {
                rootObjects[kvp.Key] = kvp.Value;
            }
            string script = string.Format("window._controllerActions={0};", JsonConvert.SerializeObject(rootObjects).Replace(@"/", @"\/"));
            return Content(script, "text/javascript");
        }

        private Dictionary<string, string> GetActionsFromController(Type controller, object routeValue = null) {
            var controllerActions = new Dictionary<string, string>();
            string controllerName = Regex.Replace(controller.Name, "Controller$", string.Empty);

            var controllerMethods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance);
            var syncActionMethods = controllerMethods.Where(m => typeof (ActionResult).IsAssignableFrom(m.ReturnType));
            var asyncActionMethods = controllerMethods
                .Where(m => m.ReturnType.IsGenericType
                    && m.ReturnType.GetGenericTypeDefinition() == typeof (Task<>)
                    && typeof (ActionResult).IsAssignableFrom(m.ReturnType.GetGenericArguments()[0]));

            foreach (var method in syncActionMethods.Union(asyncActionMethods)) {
                controllerActions[method.Name] = Url.Action(method.Name, controllerName, routeValue);
            }
            return controllerActions;
        }
    }
}

In your master page, make sure you put that script tag above all others.

<!-- Make sure you've got that query string parameter to avoid outdated client caching -->
<script src="@Url.Action("ControllerActionList", "ActionList", new {v = "1.0"})"></script>

When you’re ready to use it, simply call:

var url = _controllerActions.People.SavePerson;