<-- cd /blog

$ cat blog/mvc4-angular2-routing.md

MVC 4 + Angular 2 Routing -- Making Two Frameworks Share a URL Space

#csharp#dotnet#angular#mvc#routing#asp.net

Originally published on speedydev.pl in 2017 as part of the Daj Się Poznać blogging contest. Republished here for archival purposes.

MVC + Angular routing

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 correct ReturnUrl

Sometimes good enough is exactly right.