One of the challenges of SPA applications is making sure a bookmark or hard refresh knows how to load just enough of the content from the server before applying the client-side routing logic to that base page.

This is not guaranteed to be the only way to do this, just the one that worked for me.

Goals:
1. Static files to live in “Assets” instead of “wwwroot”
2. Client-side routes like ~/configure/userScenarios to return ~/index.html when the browser loads them
3. No extra work to remember when I add new configuration pages client-side

Program.cs – Rename WebRoot

In my Program.cs file, I renamed wwwroot to Assets:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseWebRoot("Assets")
        .UseIISIntegration()
        .UseStartup<Startup>()
        .UseApplicationInsights()
        .Build();
 
    host.Run();
}
public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseWebRoot("Assets")
        .UseIISIntegration()
        .UseStartup<Startup>()
        .UseApplicationInsights()
        .Build();

    host.Run();
}

Startup.cs – Default Files, Assets, Client Routes

Then in my Startup.cs file I added configuration to load “index.html” by default, static files in my “Assets” folder, and URL rewriting to rewrite client-side route patterns to the base “index.html” file:

C#
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 void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
   // ...
 
    // rewrite client-side routes to return index.html
    var options = new RewriteOptions()
        .AddRewrite("^testRuns.*", "index.html", skipRemainingRules: true)
        .AddRewrite("^configure/.*", "index.html", skipRemainingRules: true)
        .AddRewrite("^settings/.*", "index.html", skipRemainingRules: true);
    app.UseRewriter(options);
 
    // index.html is the default if a file isn't asked for
    app.UseDefaultFiles(new DefaultFilesOptions()
    {
        DefaultFileNames = new List<string>() { "index.html" },
        FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"Assets")),
        RequestPath = new PathString("")
    });
 
    // and all the rest of my static files live in Assets too
    app.UseStaticFiles(new StaticFileOptions()
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"Assets")),
        RequestPath = new PathString("")
    });
 
    // ...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
   // ...

    // rewrite client-side routes to return index.html
    var options = new RewriteOptions()
        .AddRewrite("^testRuns.*", "index.html", skipRemainingRules: true)
        .AddRewrite("^configure/.*", "index.html", skipRemainingRules: true)
        .AddRewrite("^settings/.*", "index.html", skipRemainingRules: true);
    app.UseRewriter(options);

    // index.html is the default if a file isn't asked for
    app.UseDefaultFiles(new DefaultFilesOptions()
    {
        DefaultFileNames = new List<string>() { "index.html" },
        FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"Assets")),
        RequestPath = new PathString("")
    });

    // and all the rest of my static files live in Assets too
    app.UseStaticFiles(new StaticFileOptions()
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"Assets")),
        RequestPath = new PathString("")
    });

    // ...
}

I needed two Nuget packages:

  • Install-Package Microsoft.AspNetCore.Rewrite
  • Install-Package Microsoft.AspNetCore.StaticFiles

And the only ongoing work as I add to my application is when I add a new client-side route pattern for a new set of pages.