Microsoft’s two major development platforms, .NET Framework and .NET Core, have converged in .NET 6, with major changes to .NET SPA web application development. You can hit F5 and be confident that everything just works, however, I couldn’t help spending some time digging into what’s going on under the hood…
Forewarning: I’m going to ignore .NET Framework web development as that was primarily a much more straightforward IIS- or IIS Express-enabled inner loop and focus on .NET Core / .NET 6+.
.NET Core 5 and Below
Previously in .NET Core developers would create a Single Page Application (SPA) in a JavaScript-based framework such as React or Angular and, during development, a Microsoft-provided middleware would proxy the request to a SPA development server:
There are a few things to call out here; the web request first hits the .NET Core request pipeline (the series of handlers you create in the Configure()
method of the Startup.cs
file), and requests are processed by each in turn. The SPA proxy is typically among the last in the pipeline, only getting triggered if none of the preceding handlers have successfully handled the request. In the above example, that means that no static files or MVC routes have been found that match the request.
This allowed you to keep things like static files and unauthenticated landing pages really quick to load, without having to load the entire SPA code into the browser for the first time.
In the case of one of my pet projects, it also meant I could put authentication and authorisation middleware before the SPA proxy, safe in the knowledge that the SPA would not be served unless the user had already authenticated, otherwise they would be redirected to the login pages which were built in MVC.
Now in .NET 6
Upgrading my pet project to MVC has been nothing short of a complete nightmare, and I truly hope Microsoft start to embrace once again their focus on backwards-compatibility because moving fast and breaking things is not appropriate for development frameworks where millions are trying to build mission-critical systems in heavily regulated industries. (Or pet projects).
My personal frustrations aside, let’s look at what’s changed…
SPA Development in .NET 6 now uses Hosting Startup Assemblies to inject a SPA Development proxy into the .NET process startup.
SECURITY WARNING: For some reason that I must be too dense to work out, and having learned nothing from the log4shell exploit vector, Microsoft have made this process injection vulnerability the default behaviour. To prevent hosting startup assemblies being injected into your processes (an important security consideration), you must either override the WebHostBuilder defaults or set an environment variable.
This proxy relies on the presence of a spa.proxy.json
file, which you won’t find anywhere in your Solution Explorer. Instead, the configuration for this proxy can be found in the .csproj file:
<SpaRoot>ClientApp\</SpaRoot>
<SpaProxyServerUrl>https://localhost:44419</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
And this is pushed into a spa.proxy.json
file during build:
{
"SpaProxyServer": {
"ServerUrl": "https://localhost:44419",
"LaunchCommand": "npm start",
"WorkingDirectory": "/home/rich/petproject",
"MaxTimeoutInSeconds": "120"
}
}
So by default you end up with 3 servers running when you hit F5:
- :5xxx – .NET process (MVC, WebAPI) exposed as HTTPS (redirects to Node server)
- :7xxx – .NET process (MVC, WebAPI) exposed as HTTP (redirects to Node server)
- :44xxx – Node server hosting SPA app exposed as HTTPS (config-based proxying to .NET)
Almost immediately you can see that the new setup is significantly more complicated (something I’m inherently allergic to).
There is a file at ClientApp/src/setupProxy.js
which creates a JavaScript HTTP proxy:
const createProxyMiddleware = require('http-proxy-middleware');
const { env } = require('process');
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:7564';
const context = [
"/weatherforecast"
];
module.exports = function(app) {
const appProxy = createProxyMiddleware(context, {
target: target,
secure: false
});
app.use(appProxy);
};
Any of the paths in the const context = []
array will be proxied to the .NET application, but there’s a big problem with this: you can either use fully qualified string paths or wildcard paths. But not both.
This means you can’t route to the images and CSS in your .NET web app’s wwwroot folder and an MVC hosted home page. This does not work:
const context = [
"/",
"/css/**",
"/images/**"
];
I’ve settled on a neat and rapid way to develop apps using MVC-hosted login pages and cookie authentication, which allows you to secure API calls and the SPA content really simply – it avoids passing, synching and refreshing access tokens because it simply uses the cookie sliding lifetime, but this now doesn’t work because the .NET request pipeline only runs AFTER the SPA has handled (i.e. proxied) the request.
Conclusion
I’m not quite sure why but Microsoft have really lost their way over the past few years. Teams is a usability cluster-fuck, the .NET Foundation has had multiple questionable incidents and resignations, .NET (Core) has seemingly shifted Microsoft’s focus from rock solid enterprise software to move-fast-and-break-things, and don’t even get me started on Azure’s myriad reliability and catastrophic security issues.
.NET 6 seems to be another example of them – or rather the ASP.NET Team in particular – forgetting their principles and roots.
I’ve been extremely vocal about my disappointment in Microsoft, not because they’re bad or I dislike the company, but because I know they can do so much better. I wouldn’t have a career without Microsoft technologies and some of my most treasured memories involve workshops at Microsoft UK in Reading and attending the BUILD conference in San Francisco. I know Microsoft can be an inspirational, high quality company again, but jeeeeeez, I really hope they start turning it around soon!