BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles ASP.NET SPA Templates Proxy Changes from .NET 5 to .NET 6, .NET 7, and on

ASP.NET SPA Templates Proxy Changes from .NET 5 to .NET 6, .NET 7, and on

Bookmarks

Key Takeaways

  • In .NET 6, the communication between front-end SPA and back-end .NET API was changed.
  • From .NET 6, the template uses the front end’s proxy solutions to send the request to the back end, resulting in a more independent back end.
  • A proxy for the development servers enables readable and debuggable code for both the front and back end.
  • Using Microsoft’s reverse proxy solution Yarp with the SpaYarp package is still a viable alternative.
  • The .NET 6 changes also apply to the following versions.

ASP.NET has many templates that help you get started with development on the .NET ecosystem. The templates offer Server Side Rendering (Razor Pages, MVC, Blazor Server) and Client Side Rendering (Blazor WebAssembly) solutions, as well as Single Page Applications (Angular, React).

This article will describe the latter one, the SPA templates, and the proxy changes introduced for the development environment from .NET 5 to .NET 6, .NET 7, and on to the next versions.

.NET 5 support was retired May 10, 2022. A breaking change that switches the way proxies are used in the ASP.NET SPA template was introduced in .NET 6 Preview 4, May 25, 2021. Users were left frustrated without a clear way to upgrade to the new proxy approach.

As this change mainly affected smaller projects and enthusiasts, the only answers were scattered in forums (few examples 1, 2, 3) with good hints and suggestions but no clear guide. A clear migration guide only appeared in November 2022 after .NET 7 was made available.

The new template changes how the communication between the front-end SPA and the back-end .NET API is done. The old templates (from .NET Core to .NET 5) used a specialized middleware that launched the development server for the front-end framework and then proxied the request from the .NET server to the front-end Node.js.

The ASP.NET SPA templates are compiled and launched differently for development and the published version.

In the final released version, Kestrel serves the requests, while for development, Node.js and Kestrel are both used, so the front-end and back-end code can be debugged more easily. The changes affect only the development environment.

[Click on the image to view full-size]

This approach meant that the launch code needed to be specific for each front-end framework, resulting in hard-to-maintain code for each front-end framework that the Microsoft team wanted to support.

[Click on the image to view full-size]

From .NET 6, the new templates for Angular and React switch how the front end and back end communicate. They use the front end’s proxy solutions to send the request to the back end. The popular front-end frameworks already have built-in support for development server proxying, but they must also be configured each specific to the used framework. The ASP.NET app still launches the front-end development server, but the request comes from that server.

Advantages of this new approach include:

  • Simpler configuration in the back-end files
  • The back end is more independent from the front-end framework but not completely separate as the launch command and URL are still specific
  • No more framework-specific code in the back end’s Startup.cs or Program.cs files
  • Extensible to other front-end frameworks not included in the templates

The logic for starting the front-end development server during development is done using Microsoft.AspNetCore.SpaProxy package and setting the front end’s URL and launch command.

        <SpaProxyServerUrl>https://localhost:44435</SpaProxyServerUrl>
        <SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>

For the front-end side, each framework has its own implementation. For example, React uses a third-party package http-proxy-middleware, which is like the following

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(
    '/api',
    createProxyMiddleware({
      target: 'http://localhost:5000',
      changeOrigin: true,
    })
  );
};

Angular has instead a more extensive configuration solution. They use a file named proxy.conf.json, where developers can define the proxy and then add it to the options in angular.json.

proxy.conf.json
{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

angular.json
…
  "architect": {
    "serve": {
      "builder": "@angular-devkit/build-angular:dev-server",
      "options": {
        "browserTarget": "your-application-name:build",
        "proxyConfig": "src/proxy.conf.json"
      },
…

One great advantage of this template is that the application is a single project: the final published version has one entry point, and the front-end code is served as static files from the ASP.NET process. This makes the deployment simpler because it is only one service that needs to be deployed and maintained. It is great for small solutions, side projects, or just trying something quickly.

The template comes with:

  • Already built pages that contain client-side navigation
  • Development server integration
  • Efficient production builds
  • Counter and fetch-data examples

Depending on the team size, it can also work for bigger projects, as under the ClientApp folder, one can work as if it were a separate front end only solution. But creating a totally separate front end is much cleaner in the long run. Creating well-defined projects help with maintaining the code. This also helps specialized developers (like React or .NET) work only with the projects they know.

Using a proxy during development has multiple benefits for the developer:

  • Forwarding requests to a specific path, like awesomeweb.com/about to localhost:3210/about
  • Working with relative paths
  • Rewriting paths
  • Resolving HTTP/HTTPS issues that could arise due to certificates
  • Solving CORS (Cross-Origin Resource Sharing) errors, without adding explicit allow rules for local devServers
  • Both front-end and back-end code is readable and can be debugged

These benefits can greatly accelerate the development process and make the developers experience with asp.net more pleasant.

Suppose you don’t like using the front-end-proxy-to-back-end approach because there is a difference when serving pages in development and production, or you are not a fan of setting up the front-end proxying. In that case, an alternative is available using Microsoft’s reverse proxy solution Yarp. The package is called SpaYarp, and the approach is a mix of using the new template configurations for the back end without flipping the proxy communication.

If you want to upgrade your existing solution to .NET 6 (LTS) or later, then the following steps will summarize what files need to be updated both on the .NET side and the Angular (annotated with a) or React (annotated with r) side.

Migration

To migrate an ASP.NET Angular or React template, the following files need to be updated:

Back end:

  • the project file
  • LaunchSettings.json
  • Startup.cs or Program.cs (depending on the setup)

Front end:

●    package.json
●    angular.json (angular only)
●    files must be added to set up the proxy and HTTPS

Back end

1. In the Nuget packages section of your project, remove the reference to Microsoft.AspNetCore.SpaServices.Extensions and add the new package called Microsoft.AspNetCore.SpaProxy.

2. In the csproj file of the project, add in the first PropertyGroup

  <SpaProxyServerUrl>https://localhost:<<Insert Frontend Port>></SpaProxyServerUrl>
  <SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>

At the end, there is a Target with a name of PublishRunWebpack. In that group, update the ResolvedFileToPublish->RelativePath to

<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>

3. In the launchSettings.json, update every profile with the property:

"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"

It should look similar to the following lines:

  "profiles": {
    "TestProject": {
...
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
      }
    ...
    },
    "IIS Express": {
...
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
      }
    ...
    }
  }

4. In the Startup.cs or Program.cs you need to remove:

●    AddSpaStaticFiles():

services.AddSpaStaticFiles(configuration =>
{
    ... //All the configs/code inside
});

●    UseSpaStaticFiles():

    app.UseSpaStaticFiles();

●    UseSpa:

app.UseSpa(spa =>
{
    ... //All the configs/code inside
});

5. You should add a fallback to "index.html" in the endpoints section, like:

app.UseEndpoints(endpoints => {
  ...
  endpoints.MapFallbackToFile("index.html");
  ...
});

Front end

Even if, for the front end part of the templates, Angular and React have different implementations, they both use the same HTTPS setup file.

1. Add aspnetcore-https.js next to the package.json file

// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate
const fs = require('fs');
const spawn = require('child_process').spawn;
const path = require('path');

const baseFolder =
  process.env.APPDATA !== undefined && process.env.APPDATA !== ''
    ? `${process.env.APPDATA}/ASP.NET/https`
    : `${process.env.HOME}/.aspnet/https`;

const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;

if (!certificateName) {
  console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
  process.exit(-1);
}

const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);

if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
  spawn('dotnet', [
    'dev-certs',
    'https',
    '--export-path',
    certFilePath,
    '--format',
    'Pem',
    '--no-password',
  ], { stdio: 'inherit', })
  .on('exit', (code) => process.exit(code));
}

For the Angular template:

2a. Update package.json

"start": "run-script-os",
"start:windows": "ng serve --port <<Insert Frontend Port>> --ssl --ssl-cert %APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem --ssl-key %APPDATA%\\ASP.NET\\https\\%npm_package_name%.key",
"start:default": "ng serve --port <<Insert Frontend Port>> --ssl --ssl-cert $HOME/.aspnet/https/${npm_package_name}.pem --ssl-key $HOME/.aspnet/https/${npm_package_name}.key",

Note: use the same port as in the back-end files.

3a. Install run-script-os with the command

npm install --save-dev run-script-os

This package adds the ability to use OS-specific operations in npm scripts as you can see above with the :windows denotation.

4a. In the angular.json file for development, add a proxyConfig property pointing to the new proxy config file:

"serve": {
  ...
  "configurations": {
    ...
    "development": {
      ...
      "proxyConfig": "proxy.conf.js"
    }
  },
  ...
},

5a. Add the proxy.conf.js file with the contents:

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:[IIS-HTTP-PORT]';

const PROXY_CONFIG = [
  {
    context: [
      "/weatherforecast",
  ],
    target: target,
    secure: false,
    headers: {
      Connection: 'Keep-Alive'
    }
  }
]

module.exports = PROXY_CONFIG;

For the react template:

2r. Update the package.json

"prestart": "node aspnetcore-https && node aspnetcore-react",
"start": "rimraf ./build && react-scripts start",

3r. Add .env.development environment file:

PORT=<<Insert Frontend Port>>
HTTPS=true

Note: use the same port as in the back-end files.

4r. Add the file aspnetcore-react.js

// This script configures the .env.development.local file with additional environment variables to configure HTTPS using the ASP.NET Core
// development certificate in the webpack development proxy.

const fs = require('fs');
const path = require('path');

const baseFolder =
  process.env.APPDATA !== undefined && process.env.APPDATA !== ''
    ? `${process.env.APPDATA}/ASP.NET/https`
    : `${process.env.HOME}/.aspnet/https`;

const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;

if (!certificateName) {
  console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
  process.exit(-1);
}

const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);

if (!fs.existsSync('.env.development.local')) {
  fs.writeFileSync(
    '.env.development.local',
`SSL_CRT_FILE=${certFilePath}
SSL_KEY_FILE=${keyFilePath}`
  );
} else {
  let lines = fs.readFileSync('.env.development.local')
    .toString()
    .split('\n');

  let hasCert, hasCertKey = false;
  for (const line of lines) {
    if (/SSL_CRT_FILE=.*/i.test(line)) {
      hasCert = true;
    }
    if (/SSL_KEY_FILE=.*/i.test(line)) {
      hasCertKey = true;
    }
  }
  if (!hasCert) {
    fs.appendFileSync(
      '.env.development.local',
      `\nSSL_CRT_FILE=${certFilePath}`
    );
  }
  if (!hasCertKey) {
    fs.appendFileSync(
      '.env.development.local',
      `\nSSL_KEY_FILE=${keyFilePath}`
    );
  }
}

5r. Finally, in the ClientApp\src folder add a setupProxy.js

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:[IIS-HTTP-PORT]';

const context =  [
  "/weatherforecast",
];

module.exports = function(app) {
  const appProxy = createProxyMiddleware(context, {
    target: target,
    secure: false,
    headers: {
      Connection: 'Keep-Alive'
    }
  });

  app.use(appProxy);
};

After all the changes, build and run the application. Both Angular and React versions should follow the new proxy strategy and have HTTPS support.

Conclusion

This new approach of using the proxies in the templates looks more robust and will probably remain in use for the next releases of the ASP.NET SPA templates.

The ease of having a starting command for the front end defined in the SpaProxyLaunchCommand and a URL linking to the front-end framework SpaProxyServerUrl makes it simpler to use other front-end frameworks than the ones available from Microsoft.

The example connections for React and Angular show possible scenarios to leverage the front end’s proxy to send requests to the back end while using it in debug mode. With this solution, you can deploy a SPA application with a front end on the same machine/instance because it is only one process, providing a quick and easy way to start development on an app with SPA as a front end.

About the Author

Rate this Article

Adoption
Style

BT