Dogs Chasing Squirrels

A software development blog

Hosting a compiled SPA on .net core

0

Let’s imagine we have a single page application (SPA) to handle the client-side logic and a .NET WebAPI application to handle the server side. It’s common to run a compiled single page application (SPA) on something like NodeJS or Vite, and Microsoft has examples of how to create a proxy application that passes SPA-related calls through to it.

Let’s imagine, though, that we want to compile our single page application and have .NET serve up the compiled files statically, without our having to run another server. Here, I’ll demonstrate how to do that. The source code for the demo is on GitHub.

First, let’s create the folders we need. I’m going to call the project “SpaDemo”. Also note that I’m running the following on Linux, so adapt the shell commands as you see fit.

Getting Started

Make the main folder.

$ mkdir SpaDemo

Server Project

In our main folder, create a folder for our .NET server project.

$ cd SpaDemo
$ mkdir server
$ cd server

Create a .NET solution:

$ dotnet new sln -n DemoServer

In it, create a new webapi project and add the project to the solution:

$ dotnet new webapi -n DemoServer
$ dotnet sln DemoServer.sln add DemoServer/DemoServer.csproj

This will create .NET’s default “WeatherForecast” app. Let’s get rid of that stuff and add a new controller that listens to the “/api/demo” route:

using Microsoft.AspNetCore.Mvc;
namespace DemoServer.Controllers; 
[ApiController]
[Route( "/api/[controller]")]
public class DemoController  : ControllerBase {
}

Imagine we want to offer up some .NET configuration data to our SPA client. One way to do this would be to add some values to appsettings.json, e.g.:

{"someName":"SomeValue"}

Then host them with a custom endpoint in our controller:

	private readonly IConfiguration _configuration;

	public DemoController(IConfiguration configuration) {
		this._configuration = configuration;
	}

	[HttpGet]
	[Route("config")]
	public IActionResult GetConfig() {
		// Create a dynamic configuration object
		var config = new {
			SomeName = this._configuration.GetValue<string>( "SomeKey" )
		};
		// Return it as JSON
		return new JsonResult( config );
	}

If we run the application now, we can go to /api/demo/config and see the following:

{"someName":"SomeValue"}

Client Project

I’m going to create the client project in Svelte.

First, go back to our “SpaDemo” folder and create the client application:

$ cd ..
$ npm create vite@latest
✔ Project name: … client
✔ Select a framework: › Svelte
✔ Select a variant: › TypeScript

Scaffolding project in /home/mike/Repos/SpaDemo/client...

Done. Now run:

  cd client
  npm install
  npm run dev

If you do as instructed, you’ll get the “Vite + Svelte” demo page with is counter application.

I want to test routing, so install the svelte-routing library:

$ npm install svelte-routing

added 1 package, and audited 97 packages in 654ms

10 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Then we’ll modify the application to a very simple 3-page application.

App.svelte:

<script lang="ts">
  import { Router, Route } from 'svelte-routing'
  import Home from "./lib/Home.svelte";
  import Page1 from "./lib/Page1.svelte";
  import Page2 from "./lib/Page2.svelte";
</script>
<main>
    <Router>
        <Route path="/">
            <Home/>
        </Route>
        <Route path="/page1">
            <Page1/>
        </Route>
        <Route path="/page2">
            <Page2/>
        </Route>
    </Router>
</main>

Home.svelte:

<script lang="ts">
    import Links from "./Links.svelte";
</script>
<h1>Home</h1>
<Links/>

Page1.svelte:

<script lang="ts">
    import Links from "./Links.svelte";
</script>
<h1>Page 1</h1>
<Links/>

Page2.svelte:

<script lang="ts">
    import Links from "./Links.svelte";
</script>
<h1>Page 2</h1>
<Links/>

Links.svelte:

<script lang="ts">
    import { Link } from 'svelte-routing'
</script>
<div>
    <Link to="/">Go Home</Link>
    <Link to="/page1">Go to Page 1</Link>
    <Link to="/page2">Go to Page 2</Link>
</div>
<style lang="css">
    div {
        display: flex;
        flex-direction: column;
    }
</style>

Run the application with npm run dev. It will show a Home page with links to Page1 and Page2, which you can then navigate around.

Compile the Client

We want to compile the client to static files and we want to put the result in the server’s wwwroot directory.

First, modify the client’s vite.config.ts file to build to our desired folder:

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [svelte()],
  build: {
    outDir: '../server/DemoServer/wwwroot',
    emptyOutDir: true,
    rollupOptions: {
      output: {
      }
    }
  }
})

Run npm run build to compile:

$ npm run build

> client@0.0.0 build
> vite build

vite v4.4.9 building for production...
✓ 42 modules transformed.
../server/DemoServer/wwwroot/index.html                  0.46 kB │ gzip: 0.29 kB
../server/DemoServer/wwwroot/assets/index-acd3aff5.css   1.09 kB │ gzip: 0.58 kB
../server/DemoServer/wwwroot/assets/index-1625dc16.js   24.76 kB │ gzip: 9.57 kB
✓ built in 560ms

If you navigate to the folder, you’ll see that we have an index.html file in the root and then some CSS and JS files in the assets folder.

Configure the Server

If you run the .NET application now and go to / you’ll get a nice 404. To tell it that it should handle static files, we’ll add “UseStaticFiles”, and to let it know that / should go to /index.html we’ll add “UseDefaultFiles”:

app.UseDefaultFiles();
app.UseStaticFiles();

Restart your .NET application, navigate to / and, voilà, you’ll have a working Svelte application. You should be able to navigate around to /page1 and /page2 and it will all work correctly.

Now try loading /page1 directly in the browser. 404! When we started at /, .NET routed that to index.html and thereafter our SPA was sneakily rewriting the URL to fake /page1 and /page2 endpoints. If we go directly to those URLs, .NET won’t know how to route it. To fix that, we finally add:

app.MapFallbackToFile( "index.html" );

Now when we go to /page1, .NET will happily pass it back to our SPA which will route it correctly.

Conclusion

We now have a compiled Svelte single-page application running as static files behind .NET with routing working as expected. Again, the source code for the demo is on GitHub.

Leave a comment