10

How do I fix my routing? I have a C# project with an Angular front-end. If I go to a c# View which calls an Angular component everything breaks. If I call an Angular view (directly from the URL) everything works fine.

C# routing to a c# view

  • If I route properly in startup.cs I go to: xxx/Home/index which is simply a View that calls an Angular component (which throws a bunch of 500 errors)

Manually routing to Angular

  • If I manually add /anything to the url (xxx/Home/Index/anything) the Angular routing takes over and everything loads fine.

Index method call

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

IndexAng.cshtml

@{
    ViewData["Title"] = "Home Page";
}

@*<script src="https://npmcdn.com/tether@1.2.4/dist/js/tether.min.js"></script>*@
@*<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>*@
<h3>Loading Ang App root:</h3>
<app-root></app-root>
<script src="~/dist/vendor.js" asp-append-version="true"></script>
@section scripts {
    <script src="~/dist/main-client.js" asp-append-version="true"></script>
}

errors when calling the c# routing: enter image description here

Configure method from Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDbContext identityContext,
                    UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
        {

#if DEBUG
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
                {
                    HotModuleReplacement = true
                });
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
#else
            app.UseExceptionHandler("/Home/Error");
#endif
            app.UseStaticFiles();
            //app.UseSession();
            app.UseAuthentication();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

                routes.MapSpaFallbackRoute(
                    name: "spa-fallback",
                    defaults: new { controller = "Home", action = "Index" });
            });

        }

screenshot of trying to navigate to main-client.js

enter image description here

Rilcon42
  • 9,584
  • 18
  • 83
  • 167
  • Make sure you set the `` tag – user184994 Sep 21 '18 at 19:03
  • @user184994 where would I put that? – Rilcon42 Sep 21 '18 at 19:18
  • Take a look at https://angular.io/guide/deployment#simplest-deployment-possible, it's mentioned in there – user184994 Sep 21 '18 at 19:20
  • @user184994 After reading the Angular docs you linked, Im confused- it seems like is for Angular to find resources. However when I point to an Angular route it finds resources fine. When I point to a c# route it cant find Angular resources (as I understood my issue). What am I missing? – Rilcon42 Sep 21 '18 at 19:51
  • Copy and paste one of the script URLs into the browser directly. The error is occurring on the server. In your startup.cs - do you have this line: app.UseStaticFiles(); – Nick Goloborodko Sep 23 '18 at 22:33
  • Copy-pasting the URL into the browser should hopefully give you a stack trace at to what is going wrong, since it's a http 500 error – Nick Goloborodko Sep 23 '18 at 22:34
  • @NickGoloborodko I added my Configure method above. What else did you want me to do? When I input the url (localhost:64672) I get the error above. and when I do localhost:64672/anything Angular routing takes over. – Rilcon42 Sep 24 '18 at 00:46
  • Could you please also post the screenshot of this url when opening in browser: http://localhost:64627/dist/vendor.js?v=V3ud5nzmno5opQM1t.... (grab the URL from the screenshot - my typing isn't very good). You should see an HTTP 500 error message with a stack trace, hopefully – Nick Goloborodko Sep 24 '18 at 00:49
  • Angular routing should only take over if no MVC controller / method matching the url is found and there isn't a static file on that path. – Nick Goloborodko Sep 24 '18 at 00:51
  • Huh, so if I wanted the Angular routing to take over (as in I never want to route through c# at all) all I have to do is remeove the controller method? – Rilcon42 Sep 24 '18 at 00:55
  • Yes, the way it works - if asp.net can't handle the request (via a controller or static file) the default route is called and angular takes over the routing at that point. Could you please also try accessing this file while in Incognito / Private mode in the browser to make sure that the authentication components don't contribute to the error? – Nick Goloborodko Sep 24 '18 at 01:04
  • @NickGoloborodko I have no freaking clue why, but when launch debug mode in chrome, then open the same tab in Incognito it defaults to localhost:port/serverlist and everything works perfectly – Rilcon42 Sep 24 '18 at 01:13
  • Maybe some odd auth cookies in your regular browser? Try another browser like MS Edge - if that works fine - something is up with Auth middle ware – Nick Goloborodko Sep 24 '18 at 01:15
  • When trying other browsers I get: Navigation error occurred – Rilcon42 Sep 24 '18 at 01:21
  • How would I default to an Angular route, without ever going through c#? assuming someone typed localhost:myport into the browser I mean? – Rilcon42 Sep 24 '18 at 01:22
  • Not too sure off the top of my head. You definitely don't want every route to be passed to angular - since then if would make the server side things redundant, as you would nto be able to call any server side code from angular – Nick Goloborodko Sep 24 '18 at 01:28
  • In my asp.net MVC / Angular projects - I just have static files and /api/ urls handled by the server - I do not have any UI MVC methods, as all UI bits are handled by the angular – Nick Goloborodko Sep 24 '18 at 01:29
  • If your Angular app has a router configuration. Add the option useHash: true when do the RouterModule import like that. ```RouterModule.forRoot(routes, { useHash: true })``` – trungk18 Sep 25 '18 at 06:58
  • The explanation can be found here. Not sure If it is your exact problem. In my MVC project, there are a lot of pages that serve Angular application. And I always do the useHash. https://knightcodes.com/angular2/2017/01/05/angular-2-routes-with-asp-net-mvc.html – trungk18 Sep 25 '18 at 07:04

1 Answers1

4

First, a small note: I faced the exact same problem, but using ASP.NET MVC 5. So, I cannot guarantee 100% that the same exact solution is going to work.

But, I'm fairly sure that the technique I'm going to describe is sound, and at the very least can be adapted with a minimal effort.

Coming to the root of the problem: in order to have ASP.NET MVC (MVC for short) and Angular 2+ happily coexist, they have to know where the routing handling responsibility is going to end for the server and where it does start for the client.

We have also to understand how MVC simplifies addresses when fed with a routing request.

The solution I advocate is in creating a single Controller, called NgController, which is going to serve all the Single Page Applications (from now on, SPAs) in your installation.

A sketch of NgController is this:

public class NgController : Controller
{
    private boolean Authenticated()
    {
        // returns true if the user is authenticated. This system can
        // (and probably should) be integrated or replaced with
        // one of the ASP.NET Core authentication modules.
        return true;
    }

    public ActionResult Index()
    {
        if (!Authenticated())
            return RedirectToAction("Login", "Authentication", new { ReturnUrl = HttpContext?.Request?.Path });

        // Index does not serve directly the app: redirect to default one
        return RedirectToAction("TheApp");
    }

    // One action for each SPA in your installment
    public ActionResult TheApp() => UiSinglePageApp("TheApp");
    public ActionResult AnotherSPA() => UiSinglePageApp("AnotherSPA");

    // The implementation of every SPA action is reusable
    private ActionResult UiSinglePageApp(string appName)
    {
        if (!Authenticated())
            return RedirectToAction("Login", "Authentication", new { ReturnUrl = HttpContext?.Request?.Path });

        return View("Ui", new SinglePageAppModel { AppName = appName });
    }
}

Really basic stuff: we just need to make sure that the default Index action does NOT serve directly any app.

Then, we need to make sure that the MVC routing works properly:

  1. is going to map /TheApp/... and /AnotherSPA/ as root routes (because we like short mnemonic URLs)
  2. is not going to mess with the part of the routing that will be handled by Angular.

Some routes and how they should behave:

  • an URL of https://server:port should redirect to https://server:port/TheApp
  • https://server:port/TheApp should be served by the TheApp action in the NgController
  • https://server:port/AnotherSPA should be served by the AnotherSPA action in the NgController
  • https://server:port/TheApp/some/token/and/subroute/for/angular/deep/linking should be served by the TheApp action in the NgController AND everything after TheApp should be kept as is and go straight to Angular Router

My choice of configuration is this (MVC 5, needs some adaptation for MVC Core 2):

public static void RegisterRoutes(RouteCollection routes)
{
    // the Ng controller is published directly onto the root,
    // the * before id makes sure that everything after the action name
    // is kept as-is
    routes.MapRoute("Ng",
        "{action}/{*id}",
        new {controller = "Ng", id = UrlParameter.Optional},
        new {action = GetUiConstraint()}
    );

    // this route allows the mapping of the default site route
    routes.MapRoute(
        "Default",
        "{controller}/{action}/{*id}",
        new { controller = "Ng", action = "Index", id = UrlParameter.Optional }
    );
}

/// <summary> The Ui constraint is to be used as a constraint for the Ng route.
/// It is an expression of the form "^(app1)|(app2)$", with one branch for
/// each possible SPA. </summary>
/// <returns></returns>
public static string GetUiConstraint()
{
    // If you don't need this level of indirection, just
    // swap this function with the result.
    // return "^(TheApp)|(AnotherSPA)$";

    var uiActions = new ReflectedControllerDescriptor(typeof(NgController))
        .GetCanonicalActions()
        .Select(x => x.ActionName.ToLowerInvariant())
        .Where(x => x != "index")
        .Select(x => "(" + x + ")");

    return string.Concat("^", string.Join("|", uiActions), "$");
}

Now the MVC routing and Controller are fine and testable for the Redirection part and for the "do-not-mess-with-everything-after-the-app-name" part. ;)

We must now setup the only bit of MVC View we need to host the Angular app. We need a single "Ui" view for the "Ng" controller. The only truly important bit of the view is the <base> tag in the head of the html document. I personally copy the dist folder into a static folder inside the MVC site, just to clarify the naming. The "static" folder is meant to contain all the (you guessed it) static content. I think that MVC creates a Content folder, by default, but I never liked that name.

MVCroot
 |-static
   |-ng
     |-TheApp (this is the dist folder for TheApp)
     |-AnotherSPA (this is the dist folder for AnotherSPA)

I also pack every js file into one single file named like the SPA, but it's not necessary.
And I don't like partials, just because I don't need layouts, so I keep everything together.
YMMV.

Ui.cshtml

@{
    Layout = null;
    var baseUrl = Url.Content("/") + Model.AppName;
    var appName = Model.AppName.ToLowerInvariant();
}
<!DOCTYPE html>
<html style="min-height: 100%;">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="icon" type="image/x-icon" href="~/static/favicon.ico">

    <title>My marvelous application</title>

    @Url.CssImportContent("static/ng/{appName}/css/main.css")

    <!-- This is the CRUCIAL bit.  -->
    <base href="@baseUrl">
</head>

<body>
    <app-root>Loading...</app-root>
    @Url.ScriptImportContent($"~static/ng/{appName}/main.js")
</body>

</html>

AAAAAnd... we are done with the MVC side.

On the client side, we just need to ensure Angular manages the routing using the appropriate defaults (do NOT enable the hash option).

And everything should just work.

Hope it helps.

Alberto Chiesa
  • 7,022
  • 2
  • 26
  • 53