How to route EVERYTHING other than Web API to /index.html

C#asp.netasp.net Mvcasp.net Mvc-4Angularjs

C# Problem Overview


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?

C# Solutions


Solution 1 - C#

Use a wildcard segment:

routes.MapRoute(
    name: "Default",
    url: "{*anything}",
    defaults: new { controller = "Home", action = "Index" }
);

Solution 2 - C#

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>

Solution 3 - C#

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

Solution 4 - C#

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");

Solution 5 - C#

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;
        }
    }
}

Solution 6 - C#

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionScott R. FrostView Question on Stackoverflow
Solution 1 - C#Trevor ElliottView Answer on Stackoverflow
Solution 2 - C#BotanManView Answer on Stackoverflow
Solution 3 - C#Sonic SoulView Answer on Stackoverflow
Solution 4 - C#JohnView Answer on Stackoverflow
Solution 5 - C#AlanView Answer on Stackoverflow
Solution 6 - C#AndreasView Answer on Stackoverflow