Resolving AngularJS paths in a ASP.NET MVC SPA and IIS

Problems when your IIS Application is not at the root

Routing in the development of a AngularJS SPA (single page application) inside an ASP.NET MVC application can be problematic. Trying to resolve relative file paths when developing locally vs deploying to a remote server can have different behaviors, especially when your deployed application does not live at the root of your IIS website. Below is the problem I encountered and my solution.

The Problem with Relative URLs

For example you may have the following navigation links inside an MVC cshtml file.

<ul>
    <li><a href="/home">Home</a></li>
    <li><a href="/pages/1">Page One</a></li>
    <li><a href="/admins">Admins</a></li>
</ul>

Then if you define your AngularJS routes with the following

$routeProvider.when('/home',
  { templateUrl: '/angularApp/home/home.html', controller: 'homeCtrl' });

$routeProvider.when('/pages/:id',
  { templateUrl: '/angularApp/pages/pages.html', controller: 'pagesCtrl' });

$routeProvider.when('/admins',
  { templateUrl: '/angularApp/admins/admins.html', controller: 'adminCtrl' });

If you deploy this to IIS as a new application (named "app" for instance) instead of under the root of the website then you will encounter the following issues:

  • The navigation links will not work on the deployed server. The links will end up point to http://server/home instead of the correct http://server/app/home.
  • The templateUrl will not resolve correctly inside AngularJS. AngularJS will look for “http://server//angularApp/admins/admins.html instead of the correct location of http://server/app/angularApp/admins/admins.html

My AngularJS SPA Visual Studio Project Structure My AngularJS SPA Visual Studio Project Structure

Solution

There are probably a few different solutions but I will outline the one that worked for me and allowed to work locally and deploy without any configuration changes. To avoid configuration changes we can use a couple tricks involving the an HTML base tag and a C# conditional compilation directive. For this to work we have to assume we will always compile in DEBUG for localhost and compile in RELEASE for remote deployment.

  • Create a base html tag and conditionally set the href value based DEBUG vs RELEASE. If in DEBUG then use href = '/' otherwise use href = '/app/'
  • Change all html links to be relative paths therefore utilizing the base url
  • In AngularJS change the $routeProvider templateUrls to prepend the base url

Here is the conditional code in the Razor file ( _Layout.cshtml)

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>@ViewBag.Title</title>
    @Styles.Render("~/bundles/css")

    @if (!Html.IsDebug()) {
       <base href="/app/" />
    } else {
       <base href="/" />
    }
</head>

Here is the AngularJS routing code. It uses jQuery to find the first base tag and retrieve the href value

var baseUrl = $("base").first().attr("href");
console.log("base url for relative links = " + baseUrl);

$routeProvider.when('/home',
  { templateUrl: baseUrl + 'angularApp/home/home.html', controller: 'homeCtrl' });

$routeProvider.when('/admins',
  { templateUrl: baseUrl + 'angularApp/admins/admins.html', controller: 'adminsCtrl' });

$routeProvider.when('/pages/:id',
  { templateUrl: baseUrl + 'angularApp/endpoints/pages.html', controller: 'pagesCtrl' });

Here is my BundleConfig.cs for reference. We can still use virtual paths.

bundles.Add(new Bundle("~/bundles/app-scripts")
  .Include("~/angularApp/app.js")

  // Modules/Components
  .Include("~/angularApp/admins/*.js")
  .Include("~/angularApp/home/*.js")
  .Include("~/angularApp/pages/*.js")
);

I arrived at this solution thanks to the answers to these SO questions