22

Angular 2 beta uses html5 routing by default. However, when you go to a component and the route changes (eg http://localhost:5000/aboutus) and you reload/refresh the page, nothing is loaded.

The issue has been raised in this post also. Most of the answers say that if we are going to pursue HTML5 routing in angular 2, then this issue of routing should be taken care of in server-side. More discussion here.

I am not sure how to handle this issue using the asp.net server environment.

Any angular 2 devs out there who also uses asp.net and encounters this issue?

PS. I'm using ASP.NET 5. My Angular 2 routes are using MVC routes.

Antikhippe
  • 6,316
  • 2
  • 28
  • 43
raberana
  • 11,739
  • 18
  • 69
  • 95

11 Answers11

19

The problem you're seeing has to do with the difference between Angular routing on the client and MVC server-side routing. You are actually getting a 404 Page Not Found error because the server does not have a Controller and Action for that route. I suspect you are not handling errors which is why it appears as if nothing happens.

When you reload http://localhost:5000/aboutus or if you were to try to link to that URL directly from a shortcut or by typing it into the address bar (deep linking), it sends a request to the server. ASP.NET MVC will try to resolve that route and in your case it will try to load the aboutusController and run the Index action. Of course, that's not what you want, because your aboutus route is an Angular component.

What you should do is create a way for the ASP.NET MVC router to pass URLs that should be resolved by Angular back to the client.

In your Startup.cs file, in the Configure() method, add an "spa-fallback" route to the existing routes:

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

    // when the user types in a link handled by client side routing to the address bar 
    // or refreshes the page, that triggers the server routing. The server should pass 
    // that onto the client, so Angular can handle the route
    routes.MapRoute(
        name: "spa-fallback",
        template: "{*url}",
        defaults: new { controller = "Home", action = "Index" }
    );
});

By creating a catch-all route that points to the Controller and View that ultimately loads your Angular app, this will allow URLs that the server does not handle to be passed onto the client for proper routing.

Brad Rem
  • 6,036
  • 2
  • 25
  • 50
  • This almost works for me, when I hit refresh the page I was on loads again however the url is dropped, so in my case if you hit refresh again it ends up back on my main login page. For instance: localhost/home - refresh once, home reloads but the url in the browser is now localhost/ ... refresh twice and it reloads the main route located at the root route... any ideas by chance? – Sean Jun 08 '16 at 14:52
  • Hmmm... I've seen this in a number of places but it's not working for me - I still get a 404 - the fallback route seems to never hit. – Rick Strahl Aug 07 '17 at 07:33
  • @RickStrahl, I would check to see that the fallback route actually points to a valid/existing MVC controller and view: does /Home/Index actually really exist? – Brad Rem Aug 07 '17 at 14:07
  • The route exists. I can access the route externally via URL just fine. But I don't get to it with a fallback route. – Rick Strahl Aug 07 '17 at 17:17
  • FWIW I'm using an app.Run() handler to handle the fallback route and that works fine and IMHO is actually much cleaner because it puts that logic in the configuration where it belongs. – Rick Strahl Aug 07 '17 at 17:18
  • Ok - figured out what the problem was. The route also had an attribute route associated with it and that made the catch-all fail. Removed the attribute route and now it works. More info in this blog post: https://weblog.west-wind.com/posts/2017/Aug/07/Handling-HTML5-Client-Route-Fallbacks-in-ASPNET-Core – Rick Strahl Aug 07 '17 at 18:57
  • @BradRem what will you do if besides new { controller = "Home", action = "Index" } you want and id(urlParameter) which is optional? – Radu Mar 08 '18 at 12:46
  • @Radu, keep in mind the difference between when MVC router is control and when the Angular router is in control. Within Angular code, the Angular router will be in charge, and outside of the Angular code (shortcut, browser address line, reload) the MVC router will take first attempt at it. You can have as many routes in MVC as you want, you just want to make the last route is a catch-all route that sends unrecognized routes onto the Angular app to be router by it. Whatever additional MapRoutes you want to have; this should be an MVC issue not an Angular one. – Brad Rem Mar 08 '18 at 17:44
  • How can i do this with MVC5 ??? i can't find configure method in Startup class? there's only Configuration method? – Fares Ayyad Jan 13 '19 at 13:21
8

In your Startup.cs add this to the Configure method. This must be before other app statements.

app.Use(async (context, next) => {
    await next();

    if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)) {
        context.Request.Path = "/index.html"; // Put your Angular root page here 
        await next();
    }
});
Jaanus
  • 16,161
  • 49
  • 147
  • 202
6

My favorite solution is to add the following code to Global.asax.cs which very smoothly and reliably takes care of the issue:

     private const string RootUrl = "~/Home/Index";
     // You can replace "~Home/Index" with whatever holds your app selector (<my-app></my-app>)
     // such as RootUrl="index.html" or any controller action or browsable route

     protected void Application_BeginRequest(Object sender, EventArgs e)
        {
            // Gets incoming request path
            var path = Request.Url.AbsolutePath;

            // To allow access to api via url during testing (if you're using api controllers) - you may want to remove this in production unless you wish to grant direct access to api calls from client...
            var isApi = path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase);
            // To allow access to my .net MVCController for login
            var isAccount = path.StartsWith("/account", StringComparison.InvariantCultureIgnoreCase);
            if (isApi || isAccount)
            {
                return;
            }

            // Redirects to the RootUrl you specified above if the server can't find anything else
            if (!System.IO.File.Exists(Context.Server.MapPath(path)))
                Context.RewritePath(RootUrl);
        }
Methodician
  • 2,396
  • 5
  • 30
  • 49
  • *slow clap* This is exactly what I needed to help me integrate a new spa into an existing, complex, old-school .NET site. We are slowly moving towards an Angular 2 spa for the entire site, and this was exactly what I needed, thank you. – Blair Connolly Apr 28 '17 at 14:59
  • @methodician exactly what i needed – pavan kumar Oct 15 '18 at 13:15
  • But this will raise a lot of issues when you try to enable bundling: ` BundleTable.EnableOptimizations = true;` – Fares Ayyad Jan 13 '19 at 13:01
5

You need use this routing in ASP.NET MVC

app.UseMvc(routes =>
{
     routes.MapRoute("Default", "{*url}",  new { @controller = "App", @action = "Index" });
});

Then you need set up SystemJS with basePath options

ZOXEXIVO
  • 920
  • 9
  • 21
  • that works, but now when I refresh i got a this: after refresh: localhost/users before: localhost/users/users – jcmordan Apr 18 '16 at 02:21
2

The feature you're looking for is URL rewrite. There are two possible ways to handle it. The classic way is to let IIS do the work, as described here:

https://stackoverflow.com/a/25955654/3207433

If you don't want to depend on IIS, you can instead handle this in the ASP.NET 5 middleware, as shown in my answer here:

https://stackoverflow.com/a/34882405/3207433

Community
  • 1
  • 1
Bill
  • 403
  • 4
  • 11
2

I'm not having any luck getting

 routes.MapRoute("Default", "{*url}",  
                  new { @controller = "App", @action = "RedirectIndex" });

to work. I still get a 404 with any client side route.

Update:
Figured out why the catch-all route wasn't working: I had an attribute route defined ([Route("api/RedirectIndex")]) and while the plain route can be directly accessed with the fallback route it didn't fire. Removing the attribute route made it work.

Another solution that seems to work just as easy as the catch-all route handler is to just create a custom handler that fires at the end of the middleware pipeline in Configure():

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

});

//handle client side routes
app.Run( async (context) =>
{
        context.Response.ContentType = "text/html";
        await context.Response.SendFileAsync(Path.Combine(env.WebRootPath,"index.html"));

});

This basically ends up being the catch-all route that simply sends index.html out over the existing URL request if there was no other handler that picked up the request.

This works nicely even in combination with IIS Rewrite rules (in which case the above just won't ever get fired.

Wrote up a blog post on this topic:

Rick Strahl
  • 17,302
  • 14
  • 89
  • 134
  • Thanks for this. Upgraded my app to .NET core and have been beating my head all afternoon trying to get the .NET 4.5 solution to work for the rewriting. This finally fixed it. – BBlake Aug 25 '17 at 18:22
  • What if my project doesn't have an index.html? I get a FileNotFoundException – Jason Honingford Aug 07 '19 at 19:41
1

Here are two more options for solving this problem. You can either add the hash location strategy to your app module.

import { LocationStrategy, HashLocationStrategy } from '@angular/common';

@NgModule({
imports: [.... ],
declarations: [...],
bootstrap: [AppComponent],
providers: [
{
  provide: LocationStrategy,
  useClass: HashLocationStrategy
}
]
})
export class AppModule { }

This option will only work for the parts of your Angular2 app that live on the Home ASP Controller

Your second option is to add routes to your ASP Controller that match your Angular 2 app routes and return the "Index" View

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    [ActionName("Angular-Route1")]
    public IActionResult AngularRoute1()
    {
        return View("Index");
    }

    public IActionResult Route2()
    {
        return View("Index");
    }
}
Allen Rufolo
  • 1,173
  • 1
  • 11
  • 14
0

Did you use:

directives: [RouterOutlet, RouterLink] in the component.

Sachin
  • 146
  • 1
  • 2
  • 8
0

apply the @ZOXEXIVO's solution then, in your _Layout.cshtml add this:

<head>
    <base href="/"/>
    .....
</had>
jcmordan
  • 1,038
  • 13
  • 20
0

You can use both the routing when you call Home/Index from angular routing.

write

Home/Index.cshtml
<my-app></my-app>

app.routing.ts
    const routes: Routes = [
        { path: '', redirectTo: '/Home/Index', pathMatch: 'full' },
        { path: 'Home/Index', component: DashboardComponent }
    ]

So When URL will be Home/Index will load the component of active url so it will load dashboard component.

Engineer
  • 300
  • 1
  • 9
0

The above selected solution did not work for me I also got 404 after following all the comments to the T. I am using an angular5 app in an MVC5 app. I use the default index landing page as the start for the angular5. My angular app is in a folder named mvcroot/ClientApp/ but on ng build it puts the distributed files in mvcroot/Dist/ by altering one setting in the .angular-cli.json file with "outDir": "../Dist"

This solution did work though.

This way only routes in the Dist directory get the fall over. Now you can hit refresh every time and exact route for the angular5 app reloads while staying on the correct component. Be sure to put the catch all first. On a side note, if using a token auth in your angular5, save the token to window.localStorage (or some other mechanism outside your angular5 app) as hitting refresh will wipe out all memory where you you maybe storing your token in a global variable. This keeps the user from having to login again if they refresh.

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Catch All",
                "dist/{*url}",
            new { controller = "Home", action = "Index", id = UrlParameter.Optional });

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }


            );
RandallTo
  • 395
  • 2
  • 11