By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement . We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account
  • Update package.json.
  • Update angular.json
  • Add files to setup the proxy and HTTPS in the angular app.
  • Update Startup.cs

  • Remove AddSpaStaticFiles() :
  • services.AddSpaStaticFiles(configuration =>
        configuration.RootPath = "ClientApp/dist";
    });
  • Remove UseSpaStaticFiles() :
  • if (!env.IsDevelopment())
        app.UseSpaStaticFiles();
    
  • Remove UseSpa:
  • app.UseSpa(spa =>
        // To learn more about options for serving an Angular SPA from ASP.NET Core,
        // see https://go.microsoft.com/fwlink/?linkid=864501
        spa.Options.SourcePath = "ClientApp";
        if (env.IsDevelopment())
            spa.UseAngularCliServer(npmScript: "start");
    });
  • Inside app.UseEndpoints(...) add:
  • app.UseEndpoints(endpoints => {
      endpoints.MapFallbackToFile("index.html");
    });

    Update the project file:

  • Replace the reference to Microsoft.AspNetCore.SpaServices.Extensions with a reference to Microsoft.AspNetCore.SpaProxy.
  • -<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="..." />
    +<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="..." />
  • Update the targets in the project files to copy the published output to the wwwroot folder:
  • <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
      <!-- Ensure Node.js is installed -->
      <Exec Command="node --version" ContinueOnError="true">
        <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
      </Exec>
      <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
      <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
      <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    </Target>
    <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
      <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
      <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
      <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --configuration production" />
      <!-- Include the newly-built files in the publish output -->
      <ItemGroup>
        <DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />
        <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
          <RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
          <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
          <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
        </ResolvedFileToPublish>
      </ItemGroup>
    </Target>
  • Include the properties to indicate what command to use to start the proxy in development as well as the url to hit:
  • <PropertyGroup>
      <SpaProxyServerUrl>https://localhost:[frontend-port (44416 for example)]</SpaProxyServerUrl>
      <SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
    </PropertyGroup>  

    Update launchSettings.json

  • Add the following environment variable to each of the projects inside the profiles property:
  • "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"

    Update package.json

  • Add the following commands inside the scripts section. Replace the frontend-port with your chosen port:
  • "start": "run-script-os",
    "start:windows": "ng serve --port [frontend-port (44416 for example)] --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 [frontend-port (44416 for example)] --ssl --ssl-cert $HOME/.aspnet/https/${npm_package_name}.pem --ssl-key $HOME/.aspnet/https/${npm_package_name}.key",
  • Add the following npm package as a dependency:
  • "run-script-os": "^1.1.6",

    Update angular.json

  • Update the serve command to use a custom proxy configuration during development:
  • "serve": {
      "configurations": {
        "development": {
          "proxyConfig": "proxy.conf.js"
    

    Add the following files in the same folder as package.json

  • aspnetcore-https.js
    // 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));
    
  • proxy.conf.js (replace [IIS-HTTP-PORT] with the IIS port defined in Properties\launchSettings.json):
    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;

    Migrating React applications from Spa Extensions

  • Update Startup.cs.
  • Update the project file.
  • Update LaunchSettings.json.
  • Update package.json.
  • Add files to setup the proxy and HTTPS in the react app.
  • Update Startup.cs

  • Remove AddSpaStaticFiles():
  • services.AddSpaStaticFiles(configuration =>
        configuration.RootPath = "ClientApp/dist";
    });
  • Remove UseSpaStaticFiles():
  • if (!env.IsDevelopment())
        app.UseSpaStaticFiles();
    
  • Remove UseSpa:
  • app.UseSpa(spa =>
        spa.Options.SourcePath = "ClientApp";
        if (env.IsDevelopment())
            spa.UseReactDevelopmentServer(npmScript: "start");
    });
  • Inside app.UseEndpoints(...) add:
  • app.UseEndpoints(endpoints => {
      endpoints.MapFallbackToFile("index.html");
    });

    Update the project file:

  • Replace the reference to Microsoft.AspNetCore.SpaServices.Extensions with a reference to Microsoft.AspNetCore.SpaProxy.
  • -<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="..." />
    +<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="..." />
  • Update the targets in the project files to copy the published output to the wwwroot folder:
  • <Target
    
    
    
    
        
     Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
      <!-- Ensure Node.js is installed -->
      <Exec Command="node --version" ContinueOnError="true">
        <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
      </Exec>
      <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
      <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
      <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    </Target>
    <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
      <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
      <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
      <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
      <!-- Include the newly-built files in the publish output -->
      <ItemGroup>
        <DistFiles Include="$(SpaRoot)build\**" />
        <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
          <RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
          <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
          <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
        </ResolvedFileToPublish>
      </ItemGroup>
    </Target>
  • Include the properties to indicate what command to use to start the proxy in development as well as the url to hit:
  • <PropertyGroup>
      <SpaProxyServerUrl>https://localhost:[frontend-port (44416 for example)]</SpaProxyServerUrl>
      <SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
    </PropertyGroup>  

    Update launchSettings.json

  • Add the following environment variable to each of the projects inside the profiles property:
  • "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"

    Update package.json

  • Add the following commands inside the scripts section. Replace the frontend-port with your chosen port:
  • "prestart": "node aspnetcore-https && node aspnetcore-react",
    "start": "rimraf ./build && react-scripts start",

    Add the following files in the same folder as package.json

  • Add `.env.development
  • PORT=[frontend-port (44416 for example)]
    HTTPS=true
    
  • aspnetcore-https.js
    // 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));
    
  • 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}`
    
  • Add setupProxy.js inside ClientApp\src (replace [IIS-HTTP-PORT] with the IIS port defined in Properties\launchSettings.json):
    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);
              

    In addition it looks like POSTing to the website that relies on the SPA files will now break?

    aspnet/Announcements#495

    I'd love to ask this on the issue but you keep locking issues and strangling collaboration in order to maintain this course. We're getting further and further away from last-known-good!

    @richstokoe here is how you can do it.

    Please advise how to workaround some of the limitations of the http-proxy-middleware. For example, in multiple path matching, you cannot use string paths and wildcard paths together. This is not backwards-compatible with the SpaServices approach.

    You have full control of how requests are matched, for example:

    const pathFilter = function (path, req) {
      return path.match('^/api') && req.method === 'GET';
    const apiProxy = createProxyMiddleware({
      target: 'http://www.example.org',
      pathFilter: pathFilter,
    });

    aspnet/Announcements#495

    I'd love to ask this on the issue but you keep locking issues and strangling collaboration in order to maintain this course. We're getting further and further away from last-known-good!

    Our announcements repo is locked as it is just the central place for folks to get updates. It is ok to file an issue in the repo explicitly.

    In addition it looks like POSTing to the website that relies on the SPA files will now break?

    aspnet/Announcements#495

    Could you give us a concrete example on how this impacts you?

    Not being able to POST to a website? OAuth2 Auth Code flow. POSTing data across micro-frontend modules. I may be misunderstanding the effect of this change in which case please correct me.

    That's not what this change does. The change only prevents the fallback route to be matched on POST requests. The fallback route only matches when no other route matches and returns the default document (index.html).

    @javiercn Thanks for the reply.

    Based on what you've documented above is this fallback not the mechanism by which SPA pages are loaded? (i.e. index.html in the public folder for the CRA client app)

    Does this mean that we can't POST to an ASPNet 7 SPA?

    Does this mean that we can't POST to an ASPNet 7 SPA?

    No, it does not mean that. It means the following:

    If you did not have a route for "/Something/else" defined on the server, the application will serve "index.html" (your SPA). In previous versions it would serve the default document independent of the verb used. That caused problems because the middleware/fallback (it does not matter whether it was the old or the new approach) would handle requests that were meant for the server instead of returning a 404.

    The change that we did, constrains when index.html is returned when the route does not match to avoid sending the default document back when the verb is not GET or HEAD.

    Maybe this table helps
    Before:

    Method Server / Route defined
  •