Skip to main content

ASP.NET Core & Angular 2 - Chapter 2 Routing

Route to a component

The Angular 2 router is a powerful tool which enables you to do just that. Specifically you can configure it to route to a component when the user enters a specific url for your site.
So in this case, we could configure it so that navigating to http://domain/hello routes directly to the hello world component.
Open up your app.module.ts file and make the following simple change in routing section.
1
2
3
4
5
6
7
8
9
10
11
imports: [
    DefaultModules, //This automatically imports BrowserModule, HttpModule, and JsonpModule too.
    RouterModule.forRoot([
        { path: '', redirectTo: 'home', pathMatch: 'full' },
        { path: 'home', component: HomeComponent },
        { path: 'counter', component: CounterComponent },
        { path: 'fetch-data', component: FetchDataComponent },
        { path: 'hello', component: HelloWorldComponent },
        { path: '**', redirectTo: 'home' }
    ])
]
Incidentally, the last route (**) means that any unrecognised url (e.g. /nothingmuchimparticular) will redirect to the home page.
Now test it out by running your site and changing the url to end with /hello.


You might be wondering how angular decides where to render the component’s html? It simply looks for the router-outlet selector and renders your component there.
In our case, the router-outlet tag is included in the main app component (app.component.html).
1
2
3
4
5
6
7
8
9
10
<div class='container-fluid'>
    <div class='row'>
        <div class='col-sm-3'>
            <nav-menu></nav-menu>
        </div>
        <div class='col-sm-9 body-content'>
            <router-outlet></router-outlet>
        </div>
    </div>
</div>
If you want to display a link to your new route you’ll need to modify your site’s navigation. As with everything else, the nav menu is its own component.
Modify navmenu.component.html as follows.
1
2
3
4
5
6
7
8
9
10
<li [routerLinkActive]="['link-active']">
    <a [routerLink]="['/fetch-data']">
        <span class='glyphicon glyphicon-th-list'></span> Fetch data
    </a>
</li>
<li [routerLinkActive]="['link-active']">
    <a [routerLink]="['/hello']">
        <span class='glyphicon glyphicon-user'></span> Hello
    </a>
</li>
Now your users can navigate to your friendly greeting…

The Problem

Back when using Angular 1 (or if you instead decide to use the Angular 2 HashLocationStrategy) your client-side routes wouldn’t ever conflict with your server-side routes because of the existence of the # in the URL. But now with the natural URL that Angular 2 composes, our server-side routes and client-side routes can be in conflict.
In our “Contact List” example above the ASP.NET MVC router would look for a List action method on a Contact controller. Is this what we want? Well… maybe. We might in fact want ASP.NET MVC to serve a specific Razor template, although we might have intended for the Angular 2 router to be used instead to take us to a specific component view. Or maybe something completely different. The point is that the server-side router doesn’t know, so we have to tell it what we want.

The Solution

Our solution has to allow the ASP.NET MVC router to handle server-side routes, and allow the Angular 2 router to handle client-side routes when appropriate. So, we need something that can tell the router which kind of route it is.
This is a great scenario for an ASP.NET MVC route constraint. All we need to do is create a class that inherits from the IRouteConstraint interface that .NET provides. Below is a simple route constraint named ServerRouteConstraint that receives a delegate in its constructor of type Func<Uri, bool>. It will look at a Uri and return a bool indicating if the URL is for a server-side route or not. The routing pipeline in ASP.NET MVC will call the Match() function for us, which just calls the delegate that we passed into the constructor. This class is left generic intentionally to allow for us to define whatever logic we want.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ServerRouteConstraint : IRouteConstraint
{
private readonly Func<Uri, bool> _predicate;
public ServerRouteConstraint(Func<Uri, bool> predicate)
{
this._predicate = predicate;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
return this._predicate(httpContext.Request.Url);
}
}

We can now add a constraint to our route definition(s). In the example below we have the typical {controller}/{action}/{id} default route using an instance of ServerRouteConstraint. Our implementation says that if the request URL path starts with /Settings we will consider it to be a server-side route. If a request URL came in with a path that started with /Contacts/List, the constraint would be false and the next route definition would be tried.

The next route definition named “angular” is a catch-all route ({*url} matches anything) and will use whatever default controller & action we tell it to. This should be set to the view where you bootstrap your Angular 2 app. What this means is that the ASP.NET MVC router will not try to find a List action method on a Contact controller, but rather it will end up passing it on to the Angular router.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "default",
url: "{controller}/{action}/{id}",
defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional},
// Set a constraint to only use this for routes identified as server-side routes
constraints: new
{
serverRoute = new ServerRouteConstraint(url =>
{
return url.PathAndQuery.StartsWith("/Settings",
StringComparison.InvariantCultureIgnoreCase);
})
});
// This is a catch-all for when no other routes matched. Let the Angular 2 router take care of it
routes.MapRoute(
name: "angular",
url: "{*url}",
defaults: new { controller = "Home", action = "Index" } // The view that bootstraps Angular 2
);

}


The nice thing about the ServerRouteConstraint is that you can implement whatever logic you want to determine if a URL is for a server-side route. Maybe for your Angular 2 route setup all your routes start with /app. In that case the delegate you pass to ServerRouteConstraint could be something like this:
1
2
3
4
5
url =>
{
return !url.PathAndQuery.StartsWith("/app", StringComparison.InvariantCultureIgnoreCase);
}

This setup has been the best way for me to accomplish having my Angular 2 routes and my ASP.NET MVC routes play nicely together

Next time we’ll get on with building a more useful component.

Comments