8

With RC1 of ASP.NET Core 1.0's MVC 6 you can map routes from within your Startup.Configure function when invoking app.UseMvc. I have mapped a "spa-fallback" route that will ensure that the HomeController and Index view are the defaults like so:

public void Configure(IApplicationBuilder app, 
                      IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
    // ... omitted for brevity
    app.UseExceptionHandler("/Home/Error");
    app.UseStatusCodePagesWithRedirects("/Home/Error/{0}");

    app.UseMvc(routes =>
    {
        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
        routes.MapRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" });
        routes.MapWebApiRoute("defaultApi", "api/{controller}/{id?}");
    });
}

I desire the fallback so that my Angular2 app's routes will not result in an HTTP Status Code of 404, Not Found. But I also need to correctly handle when a user does inadvertently attempt to navigate to a page view that doesn't exist. You might notice that I have also called app.UseStatusCodePagesWithRedirects("/Home/Error/{0}");.

The call to redirect to my error page with the status code and the "spa-fallback" route seem mutually exclusive -- meaning it appears that I can only have one or the other (but sadly not both). Does anyone know how I could manage to have the best of both worlds?

David Pine
  • 23,787
  • 10
  • 79
  • 107

3 Answers3

14

It took me some time to figure out how to do this without serving my index using MVC and to still receive 404s for missing files. Here's my http pipeline:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseDefaultFiles();
        app.UseStaticFiles();

        app.UseMvc(                
            routes => {
                routes.MapRoute(
                    name: "api",
                    template: "api/{controller}/{action}/{id?}");

                // ## commented out since I don't want to use MVC to serve my index.    
                // routes.MapRoute(
                //     name:"spa-fallback", 
                //     template: "{*anything}", 
                //     defaults: new { controller = "Home", action = "Index" });
        });

        // ## this serves my index.html from the wwwroot folder when 
        // ## a route not containing a file extension is not handled by MVC.  
        // ## If the route contains a ".", a 404 will be returned instead.
        app.MapWhen(context => context.Response.StatusCode == 404 &&
                               !Path.HasExtension(context.Request.Path.Value),
                    branch => {
                           branch.Use((context, next) => {
                               context.Request.Path = new PathString("/index.html");
                               Console.WriteLine("Path changed to:" + context.Request.Path.Value);
                               return next();});

                    branch.UseStaticFiles();
        });            
    }
James Cramer
  • 455
  • 3
  • 16
  • Thank you for looking into this, I should have updated my question or deleted it earlier -- since I had already figured it out after the fact. – David Pine Mar 03 '16 at 13:00
  • This solution is nice since it doesn't require having MVC. I am currently using ASP.NET as a static file server (no MVC), but I do want to be able to reroute non asset paths to `index.html`. – Micah Zoltu Jun 14 '16 at 04:25
  • 1
    Nice answer I recommend using slightly better code for the When predicate: `context => context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)` – Igor Soloydenko Jun 22 '16 at 21:07
  • thanks! I'll update the answer to include this improvement. – James Cramer Jun 24 '16 at 19:17
  • 1
    In my case, I am not using any MVC routes - other than static files, all logical paths should route to index.html. So, I first removed the UseMvc statement. And then, I had to remove the `context.Response.StatusCode == 404 &&` part as I was getting a 200 for all requests otherwise. – sudheeshix Jan 27 '17 at 05:16
  • I'm using Mvc but still StatusCode 404 check isn't working. I think its probably cause I'm getting 404 on the page but 200 on favicon cause of Chrome. – Ahmad Oct 01 '17 at 18:48
  • I've the same problem in mvc 5. Does anyone have an idea how to solve it? Thanks! – bernnabe Dec 14 '17 at 21:12
5

I came up with two possible workarounds/solutions.

ASP.NET Core 1.0, RC1

With Angular2 specifically, we can simply swap out the LocationStrategy. For example in my wwwroot/app/boot.ts I have the following:

import {provide} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";
import {LocationStrategy, HashLocationStrategy, ROUTER_PROVIDERS} from "angular2/router";
import {HTTP_PROVIDERS} from "angular2/http";

import {AppComponent} from "./app.component"

bootstrap(AppComponent,
    [
        ROUTER_PROVIDERS,
        HTTP_PROVIDERS,
        provide(LocationStrategy, { useClass: HashLocationStrategy })
    ]);

Note that I am utilizing the provide function to override the standard LocationStrategy with the HashLocationStrategy. This adds the # in the URL and prevents MVC from incorrectly throwing 404's, but also correctly handles responding with 404's when the user manually types in a URL that doesn't exist.

ASP.NET Core 1.0, RC2

Simply use Microsoft.AspNet.NodeServices. Within this library there are both AngularServices and ReactServices which allow for the mapping of SPA routes specifically, via the MapSpaFallbackRoute function. I state that we need to wait for RC2 with the understanding that the current implementations are not fully functioning as desired.

David Pine
  • 23,787
  • 10
  • 79
  • 107
  • Thanks for letting me know about this, Im going to check it out. On the angular side, I didn't want to use hash location strategy since it isn't required with html5 browsers. – James Cramer Mar 03 '16 at 17:43
0

Try this:

public void Configure(IApplicationBuilder app,
    IHostingEnvironment env,
    ILoggerFactory loggerFactory)
{
    app.UseExceptionHandler("/Home/Error");
    app.UseStatusCodePagesWithRedirects("/Home/Error/{0}");

    app.UseMvc(routes =>
    {
        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
        routes.MapWebApiRoute("defaultApi", "api/{controller}/{id?}");
    });

    app.Run(async context =>
    {
        context.Response.Redirect("/Home/Index");
        await Task.CompletedTask;
    });
}

app.Run() will catch all requests that doesn't match any of the routes defined earlier. This way you can get your custom spa fallback and use app.UseStatusCodePagesWithRedirects() at the same time.

ceferrari
  • 1,597
  • 1
  • 20
  • 25