Alica's dev blog
Hosting SPA From ASP.NET Core 3.1 WebApi

I thought that if I work on the backend, I wouldn’t need to know anything about the frontend. Like, I would create and API and the frontend would know about it and do everything that’s needed, and I wouldn’t even know it existed.

However, in some scenarios, in order for them to work together, some (although limited) contact is needed. Let’s look at it.

Problem

We would like to host an SPA (Angular) frontend from our ASP.NET Core 3.1 WebApi.

The requirements are:

  1. Routes to all endpoints start with api/ prefix.
  2. Swagger documentation generated by Swashbuckle still works correctly.
  3. Everything that doesn’t have this prefix will be taken care for by the SPA.
  4. SPA static files are served from a folder inside the WebApi project.

Do we really need that?

You may ask if this is actually a good thing to do, and it’s a legit question.

There are other ways how to connect backend and fronted together, e.g. running them as separate services in Kubernetes (this is actually the only way that I have known before).

However, there may be business limitations that prevent you from doing it this way, e.g. you can’t use Docker on your work computer or can’t easily get access to some development Kubernetes cluster.

So, let’s act as if those 4 aforementioned points are requirements given from outside and we can’t influence them.

Looking for a solution

I spent a non-trivial amount of time googling for the solution, but either I was not using the right keywords, or all the code examples that I found were solving problem that was more complicated than mine, therefore they contained code that was redundant for me (and I couldn’t clearly identify the redundant parts).

I also discovered that it probably used to be much more difficult (in terms of the amount of the code) in earlier versions of .NET Core.

After some time, I found a post on some Microsoft dev blog that provided answers to issues 3 and 4 which required only a couple of lines of code. Unfortunately I didn’t bookmark it so I can’t link it from here.

Solution

Add prefix to all routes

To my surprise, it isn’t possible to define route prefix for all routes in a single place – at least not in ASP.NET Core (ASP.NET has such option) and with attribute routing (I didn’t investigate what’s possible with other types of routing).

I found and tried a number of hacky solutions from SO, but they all had the same problem in common: the actual route change happened only after the Swagger documentation was generated. Therefore the documentation didn’t contain the prefix – and we don’t want that.

It looks like the simplest solutions is to use route prefixes for controllers. If some endpoints have hardcoded routes (starting with /), then you have to manually add the prefix there.

[ApiController]
[Route("api/[controller]")]
public class BooksController : AbstractControllerBase

Adjust Swagger

The simplest example of usage (from the documentation) looks like this:

public void ConfigureServices(IServiceCollection services)
{
	...
	
	services.AddSwaggerGen(c => c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });

	...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	...

	app.UseSwagger();
	
	app.UseSwaggerUI(c =>
	{
		c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
		c.RoutePrefix = string.Empty;
	});

	...
}

We need to make changes only in the arguments of UseSwagger and UseSwaggerUI methods and basically tell them all that they should now live behind the api/ prefix.

First, change the RouteTemplate options parameter of UseSwagger method and add the api/ prefix to its default value /swagger/{documentName}/swagger.json.

Secondly, add the prefix to the SwaggerEndpoint options parameter of UseSwaggerUI method.

And thirdly, change the RoutePrefix options parameter of UseSwaggerUI method from empty to the prefix.

So the result looks like the following:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	...

	app.UseSwagger(c => c.RouteTemplate = "/api/swagger/{documentname}/swagger.json");
	
	app.UseSwaggerUI(c =>
	{
		c.SwaggerEndpoint("/api/swagger/v1/swagger.json", "My API V1");
		c.RoutePrefix = "api";
	});

	...
}

The documentation page is now accessible at [your base url]/api instead of [your base url].

Host the SPA

And finally, let’s host that SPA!

We’ll need Microsoft.AspNetCore.SpaServices.Extensions package for that.

Let’s add the following method calls to Startup.cs file:

public void ConfigureServices(IServiceCollection services)
{
	...
	
	services.AddSpaStaticFiles(options => options.RootPath = "SpaFolderName");
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	...

	app.UseSpaStaticFiles();

	app.UseSpa(spa => spa.Options.SourcePath = "SpaFolderName");
}

Yes, in the end, that was the simplest part :-)


Last modified on 2020-12-23