MVC 4 + Angular 2 Routing -- Making Two Frameworks Share a URL Space
Originally published on speedydev.pl in 2017 as part of the Daj Się Poznać blogging contest. Republished here for archival purposes.

I’ve been heads-down lately, but here’s a practical one from a recent adventure.
I’m stubborn: while everyone else was doing Angular 2 with MVC 5, this project needed Angular 2 with MVC 4. The routing rules were:
/front/*→ Angular 2, authenticated users only- Everything else → MVC with its own auth
The Problem
Angular handles its own routes, MVC handles the rest. Splitting them via MapRoute worked fine for the happy path — user logs in, then navigates within the app. But if a user had a direct link to /front/xza and wasn’t authenticated, they got a 404. Brutal.
What we actually needed: a redirect to /Login?ReturnUrl=%2Ffront%2Fxza.
The Solution: ServerRouteConstraint
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)
{
if (routeDirection == RouteDirection.IncomingRequest)
return this._predicate(httpContext.Request.Url);
return true;
}
}
Register two routes with constraints:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { serverRoute = new ServerRouteConstraint(url => !isFront(url)) }
);
routes.MapRoute(
name: "angular",
url: "{*url}",
defaults: new { controller = "Home", action = "FrontIndex" },
constraints: new { serverRoute = new ServerRouteConstraint(url => isFront(url)) }
);
}
private static bool isFront(Uri url)
{
return url.PathAndQuery.StartsWith("/front", StringComparison.InvariantCultureIgnoreCase);
}
The Catch: No Session at Routing Time
The isFront predicate runs at route resolution time, before the Session is available. That’s why the Angular catch-all lands on FrontIndex instead of directly on Index — the authentication check needs to happen in a controller action where Session is available:
[HDAuthorization]
public ActionResult Index()
{
ViewBag.Title = "Use angular and have fun";
return View("Index");
}
public ActionResult FrontIndex()
{
if (HDAuthorization.IsLoginUser())
return Index();
else
return RedirectToAction("Login", new { ReturnUrl = Request.Path });
}
public ActionResult Login()
{
ViewBag.Title = "Login Page";
return View();
}
[HttpPost]
public ActionResult Login(LoginModel model)
{
var loginReturn = loginManager.Login(model);
if (loginReturn.Authenticated)
{
if (Request.Path.StartsWith("/front/"))
return Redirect(Request.Path);
string requestParam = Request.Params["ReturnUrl"];
if (string.IsNullOrWhiteSpace(requestParam))
return RedirectToAction("Index");
return Redirect(requestParam);
}
ViewBag.LoginError = loginReturn.FailureText;
return View();
}
Angular routes are configured the usual way:
const appRoutes: Routes = [
{ path: 'front/requests', component: AppRequests },
{ path: 'front/request/:id', component: AppRequest },
// ...
];
Result
Not the prettiest solution in the world, but it does exactly what’s needed:
/front/*is cleanly Angular’s domain- Everything else is MVC’s
- Unauthenticated users hitting a deep
/front/link get redirected to login with the correctReturnUrl
Sometimes good enough is exactly right.