.Net Core MVC Routing: Setting MapSpaFallbackRoute Conditionally Per Controller

David picture David · Oct 4, 2016 · Viewed 11k times · Source

I am developing a project that involves multiple Single Page Applications that are hosted on a single .Net Core 1.0 web app.

The goal is to partition each SPA so that they all exist separately, each with their own View and Controller.

In Startup.cs, am using Microsoft.AspNetCore.SpaServices MapSpaFallbackRoute to pass off the deep links to the SPAs (in this case Angular 2), so MVC doesn't get confused and throw a 404 on page refresh.

I have it working for one SPA at a time when I set the Controller manually in MapSpaFallbackRoute, but I can't figure out a way set it conditionally for each SPA Controller.

I'm assuming that a combination of Mapwhen() and Run() will get me where I need to be, but I can't seem to get the syntax correct.

Assuming the SPAs are named 'Dash1', 'Dash2', etc. The following fallback route works perfectly for Dash1:

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

            routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Dash1", action = "Index" });
        });

I've tried this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {

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

    }

    private static void HandleDash1(IApplicationBuilder app)
    {
        app.UseMvc(routes =>
        {
            routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Dash1", action = "Index" });
        });
    }
}

...But then when I navigate to Dash1 and try to refresh the page, the browser prepends '/Dash1/' to all of the http requests and breaks everything.

Answer

David picture David · Oct 12, 2016

I got it to work with MapWhen(), but each SPA will have to be registered this way.

app.MapWhen(context => context.Request.Path.Value.StartsWith("/Dash1"), builder =>
{
    builder.UseMvc(routes =>
    {
        routes.MapSpaFallbackRoute("dash1-fallback", new { controller = "Dash1", action = "Index" });
    });
});

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

I am using this method to host Angular(2+) apps. I was originally using SystemJS and keeping all of the apps in one project, but I moved to Angular CLI when version 4 was released.

The best way I've found to manage the apps is to create a separate project in the solution for each app and use NPM scripts to copy the bundled files into a folder inside wwwroot of the "master" project.

Instead of returning a View, I'm just serving the static html file created by the CLI but Views work fine too.

public IActionResult Index()
{
    return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/dash1", "index.html"), "text/HTML");
}

The tricky part is managing the routers. You must set the base href and hand off routing to the Angular router by setting window['_app_base'] to correspond to the current controller's route.

Include the following in index.html:

<head>
    // Set base href. 
    // You need to include the path if serving the static file from wwwroot
    // Use '/' if using a View (I think, I had to change this when I switched to static files).

    <base href="/dash1/"> 

    <script>
        (function () {
            // You could type the controller path here again but this will infer it.

            window['_app_base'] = '/' + window.location.pathname.split('/')[1];

        })();
    </script>
</head>

Then, the Angular router has to get it's bearings from the window['_app_base']. In the app.module.ts include the following:

import { NgModule, Provider } from '@angular/core';
import { APP_BASE_HREF, Location } from '@angular/common';
const baseHref: Provider = {
    provide: APP_BASE_HREF,
    useValue: window['_app_base'] || '/'
};

Finally, Include baseHref in the providers array of @NgModule.