56

I've been working on an AngularJS project, inside of ASP.NET MVC using Web API. It works great except when you try to go directly to an angular routed URL or refresh the page. Rather than monkeying with server config, I thought this would be something I could handle with MVC's routing engine.

Current WebAPIConfig:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional },
            constraints: new { id = @"^[0-9]+$" }
        );

        config.Routes.MapHttpRoute(
            name: "ApiWithActionAndName",
            routeTemplate: "api/{controller}/{action}/{name}",
            defaults: null,
            constraints: new { name = @"^[a-z]+$" }
        );

        config.Routes.MapHttpRoute(
            name: "ApiWithAction",
            routeTemplate: "api/{controller}/{action}",
            defaults: new { action = "Get" }
        );
    }
}

Current RouteConfig:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute(""); //Allow index.html to load
        routes.IgnoreRoute("partials/*"); 
        routes.IgnoreRoute("assets/*");
    }
}

Current Global.asax.cs:

public class WebApiApplication : HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        GlobalConfiguration.Configure(WebApiConfig.Register);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        var formatters = GlobalConfiguration.Configuration.Formatters;
        formatters.Remove(formatters.XmlFormatter);
        GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings
        {
            Formatting = Formatting.Indented,
            PreserveReferencesHandling = PreserveReferencesHandling.None,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
        };
    }
}

GOAL:

/api/* continues to go to WebAPI, /partials/, and /assets/ all go to file system, absolutely anything else gets routed to /index.html, which is my Angular single page app.

--EDIT--

I seem to have gotten it working. Added this to the BOTTOM OF RouteConfig.cs:

 routes.MapPageRoute("Default", "{*anything}", "~/index.html");

And this change to the root web.config:

<system.web>
...
  <compilation debug="true" targetFramework="4.5.1">
    <buildProviders>
      ...
      <add extension=".html" type="System.Web.Compilation.PageBuildProvider" /> <!-- Allows for routing everything to ~/index.html -->
      ...
    </buildProviders>
  </compilation>
...
</system.web>

However, it smells like a hack. Any better way to do this?

chridam
  • 100,957
  • 23
  • 236
  • 235
Scott R. Frost
  • 2,026
  • 1
  • 22
  • 25
  • 2
    As a note for those trying (and for whom the answers below aren't ideal), the hack listed in this question works, BUT not if the path the user enters also happens to be a folder. So, for example, if you route everything to `~/index.html`, then the path `/whatever/` will only route there IF you don't have a folder path within your application called `/whatever/`. – jonnybot Apr 13 '16 at 13:24
  • 1
    Any ideas how to solve for this caveat? – Danny Mar 09 '17 at 15:37
  • Thanks for this from a guy having to cram Angular into an older MVC app – Stuart Allen Dec 02 '20 at 20:03

6 Answers6

27

Use a wildcard segment:

routes.MapRoute(
    name: "Default",
    url: "{*anything}",
    defaults: new { controller = "Home", action = "Index" }
);
Trevor Elliott
  • 11,292
  • 11
  • 63
  • 102
  • 4
    A wildcard might work, can I make it go to a flat file rather than a MVC controller? Alternatively, how would I make a Home controller with an Index action that passed execution to index.html while maintaining whatever they entered as the URL (for example /something/edit/123)? – Scott R. Frost Oct 28 '13 at 20:23
  • 1
    In ASP.NET MVC a request always goes to a controller and a controller uses a view engine to generate a view. This can be a simple HTML file with nothing special, see the [Controller.View](http://msdn.microsoft.com/en-us/library/system.web.mvc.controller.view(v=vs.108).aspx). Just rename the file to .cshtml, place it in the Views/Home folder, and set the Layout to null at the top of the file. – Trevor Elliott Oct 28 '13 at 20:43
  • The route in my answer will maintain whatever URL the user requested. – Trevor Elliott Oct 28 '13 at 20:44
  • This isn't web api... is it? – Zach Smith Mar 22 '18 at 15:14
12

Suggest more native approach

<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404" subStatusCode="-1"/>
        <error statusCode="404" prefixLanguageFilePath="" path="/index.cshtml" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>
BotanMan
  • 1,357
  • 12
  • 25
  • worked perfect and is more in line with how other web servers would be configured. Thanks! – parliament Jul 28 '15 at 02:22
  • 1
    Not sure if this is "the right way", but after a long search for other methods, I came back to this one. Best one so far. – Silencer Jan 15 '16 at 09:28
  • 1
    Worked for me as well, Since I am using only ASP.NET API, I don't have the route config available, as it is only available in System.Web.Mvc. this works perfectly. Thank you! – codea Jan 24 '16 at 22:42
  • i'm getting "this type of page is not served" error after using this approach – Sonic Soul Oct 04 '16 at 19:53
5

in my case none of these approaches worked. i was stuck in 2 error message hell. either this type of page is not served or some sort of 404.

url rewrite worked:

<system.webServer>
    <rewrite>
      <rules>
        <rule name="AngularJS" stopProcessing="true">
          <match url="[a-zA-Z]*" />

          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
            <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
          </conditions>
          <action type="Rewrite" url="/" />
        </rule>
      </rules>
    </rewrite>
    ...

notice i matched on [a-zA-Z] because i don't want to rewrite any of the .js .map etc urls.

this worked in VS out of hte box, but in IIS you may need to install a url-rewrite module https://www.iis.net/downloads/microsoft/url-rewrite

Sonic Soul
  • 23,855
  • 37
  • 130
  • 196
5

I had a similar approach as the top few answers, but the downside is if someone is making an API call incorrectly, they'll end up getting that index page back instead of something more useful.

So I've updated mine such that it will return my index page for any request not starting with /api:

        //Web Api
        GlobalConfiguration.Configure(config =>
        {
            config.MapHttpAttributeRoutes();
        });

        //MVC
        RouteTable.Routes.Ignore("api/{*anything}");
        RouteTable.Routes.MapPageRoute("AnythingNonApi", "{*url}", "~/wwwroot/index.html");
John
  • 17,163
  • 16
  • 65
  • 83
  • This answer worked for me. I added the 2x RouteTable configurations to my global.asax after the web api configuration and I registered the .html PageBuilderProvider and it worked. Didn't need to register any additional web api routes apart from the default /api route. My angular 4 app deployed in the root of the application works perfectly with routing, all bundles, images and other files also behave as expected. Thumbs up! – Hawk May 25 '17 at 05:51
1

I've been working with OWIN Self-Host and React Router few days ago, and incurred to probably similar issue. Here is my solution.

My workaround is simple; check if it's a file in the system; otherwise return index.html. Since you don't always want to return index.html if some other static files are requested.

In your Web API config file:

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

config.Routes.MapHttpRoute(
    name: "Default",
    routeTemplate: "{*anything}",
    defaults: new { controller = "Home", action = "Index" }
);

and then create a HomeController as follwing...

public class HomeController: ApiController
{
    [HttpGet]
    [ActionName("Index")]
    public HttpResponseMessage Index()
    {
        var requestPath = Request.RequestUri.AbsolutePath;
        var filepath = "/path/to/your/directory" + requestPath;

        // if the requested file exists in the system
        if (File.Exists(filepath))
        {
            var mime = MimeMapping.GetMimeMapping(filepath);
            var response = new HttpResponseMessage();
            response.Content = new ByteArrayContent(File.ReadAllBytes(filepath));
            response.Content.Headers.ContentType = new MediaTypeHeaderValue(mime);
            return response;
        }
        else
        {
            var path = "/path/to/your/directory/index.html";
            var response = new HttpResponseMessage();
            response.Content = new StringContent(File.ReadAllText(path));
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
            return response;
        }
    }
}
Alan
  • 21
  • 2
  • Commenting in hopes that it will help for search engine indexing if anyone is looking for a solution for [VueJS Router](https://router.vuejs.org/). This worked absolutely perfectly for how I'm implementing this in C# w/ ApiController. – Erutan409 Sep 16 '21 at 21:53
0

Well, I just removed the RouteConfig.RegisterRoutes(RouteTable.Routes); call in Global.asax.cs and now whatever url I enter, if the resource exists, it will be served. Even the API Help Pages still work.

Sonic Soul
  • 23,855
  • 37
  • 130
  • 196
Andreas
  • 1,551
  • 4
  • 24
  • 55
  • The behavior you describe is expected, but not what the question asks for. Sure, you can serve static resources. The question is, how to map arbitrary routes to a specific static resource (index.html). – jonnybot Apr 12 '16 at 21:55
  • sorry made the edits to wrong answer, and now removed them :) – Sonic Soul Oct 04 '16 at 21:11