Frontend & UISûr8585/100
Migrating Spa Services To Spa Proxy (Microsoft)
Migrates ASP.NET Core projects from the obsolete Microsoft.AspNetCore.SpaServices.Extensions to Microsoft.AspNetCore.SpaProxy for Angular and React SPAs. Use ONLY when Microsoft.AspNetCore.SpaServices.Extensions has been flagged as obsolete or deprecated and must be replaced — not for version-bump scenarios where SpaServices.Extensions is still supported.
ou envoie-le directement à ton agent.
Installer dans ton projet
$ curl -fsSL https://arboristhree.xyz/cli/install-skill.mjs -o /tmp/arboris-install-skill.mjs && node /tmp/arboris-install-skill.mjs migrating-spa-services-to-spa-proxy__microsoft-upgrade-agent-plugins__plugins-upgrade-agent-extenders-upgrade-dotnet-upgrade-skills-lazy-libraries-8588cc0e98efMicrosoft
microsoft/upgrade-agent-plugins
Contenu à copier
---
name: migrating-spa-services-to-spa-proxy
description: >
Migrates ASP.NET Core projects from the obsolete
Microsoft.AspNetCore.SpaServices.Extensions to
Microsoft.AspNetCore.SpaProxy for Angular and React SPAs.
Use ONLY when Microsoft.AspNetCore.SpaServices.Extensions
has been flagged as obsolete or deprecated and must be
replaced — not for version-bump scenarios where
SpaServices.Extensions is still supported.
metadata:
origin: Microsoft
discovery: lazy
traits: .NET|CSharp|DotNetCore
---
# SpaServices to SpaProxy Migration
## Overview
Migrate ASP.NET Core projects from `Microsoft.AspNetCore.SpaServices.Extensions` to `Microsoft.AspNetCore.SpaProxy`. The backend no longer manages the SPA dev server inline via `UseSpa()`; instead, `SpaProxy` automatically launches the frontend dev server and proxies requests to it. This removes all SpaServices middleware from `Startup.cs` and moves configuration to the project file and hosting startup assembly.
Covers both **Angular** and **React** frontends. Steps are shared unless marked framework-specific.
## Workflow
```
Migration Progress:
- [ ] Step 1: Detect SpaServices usage and identify frontend framework
- [ ] Step 2: Update Startup.cs
- [ ] Step 3: Update the project file
- [ ] Step 4: Update launchSettings.json
- [ ] Step 5: Update package.json
- [ ] Step 6: Update angular.json (Angular only)
- [ ] Step 7: Add HTTPS and proxy configuration files
- [ ] Step 8: Build and verify
```
### Step 1: Detect SpaServices Usage
Scan the project for:
- Package references to `Microsoft.AspNetCore.SpaServices.Extensions`
- Calls to `AddSpaStaticFiles()`, `UseSpaStaticFiles()`, `UseSpa()`
- `spa.UseAngularCliServer()` (Angular) or `spa.UseReactDevelopmentServer()` (React)
Identify the frontend framework — Angular projects have `angular.json`, React projects have `react-scripts` in `package.json`. This determines which steps and files apply.
### Step 2: Update Startup.cs
Remove three blocks of SpaServices middleware. No replacement middleware is needed — `SpaProxy` activates via a hosting startup assembly.
**Remove `AddSpaStaticFiles()`:**
```csharp
// DELETE this entire block
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
```
**Remove `UseSpaStaticFiles()`:**
```csharp
// DELETE this entire block
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
```
**Remove `UseSpa()` (Angular variant):**
```csharp
// DELETE this entire block
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
```
**Remove `UseSpa()` (React variant):**
```csharp
// DELETE this entire block
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
```
**Add fallback route** inside `app.UseEndpoints(...)`:
```csharp
app.UseEndpoints(endpoints =>
{
// ... existing endpoints ...
endpoints.MapFallbackToFile("index.html");
});
```
### Step 3: Update the Project File
**Replace the package reference:**
```diff
-<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="..." />
+<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="..." />
```
**Add SpaProxy properties** (choose a frontend port that does not conflict with the backend port):
```xml
<PropertyGroup>
<SpaProxyServerUrl>https://localhost:44416</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
</PropertyGroup>
```
**Update MSBuild targets** to ensure Node.js availability and publish the SPA output to `wwwroot`:
For **Angular** (output goes to `dist\` and `dist-server\`):
```xml
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<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">
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --configuration production" />
<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>
```
For **React** (output goes to `build\`):
```xml
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<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">
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
<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>
```
### Step 4: Update launchSettings.json
Add the hosting startup assembly environment variable to **each profile** inside the `profiles` section:
```json
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
```
Ensure the backend `applicationUrl` port differs from the frontend `SpaProxyServerUrl` port.
### Step 5: Update package.json
#### Angular
Add cross-platform start scripts with SSL using the ASP.NET Core dev certificate. Replace the frontend port to match `SpaProxyServerUrl`:
```json
"start": "run-script-os",
"start:windows": "ng serve --port 44416 --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 44416 --ssl --ssl-cert $HOME/.aspnet/https/${npm_package_name}.pem --ssl-key $HOME/.aspnet/https/${npm_package_name}.key",
```
Add the `run-script-os` dependency:
```json
"run-script-os": "^1.1.6"
```
#### React
Update start scripts to run HTTPS setup before launching the dev server:
```json
"prestart": "node aspnetcore-https && node aspnetcore-react",
"start": "rimraf ./build && react-scripts start"
```
### Step 6: Update angular.json (Angular Only)
Add a proxy configuration to the development serve target:
```json
"serve": {
"configurations": {
"development": {
"proxyConfig": "proxy.conf.js"
}
}
}
```
### Step 7: Add HTTPS and Proxy Configuration Files
Add these files in the same directory as `package.json` (typically `ClientApp/`).
#### aspnetcore-https.js (Angular and React)
Sets up HTTPS using the ASP.NET Core development certificate:
```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 (Angular Only)
Proxies API requests to the ASP.NET Core backend. Replace `[IIS-HTTP-PORT]` with the IIS Express port from `Properties/launchSettings.json`, and update the `context` array with the project's API route prefixes:
```js
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;
```
#### aspnetcore-react.js (React Only)
Configures `.env.development.local` with SSL certificate paths for the React dev server:
```js
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}`
);
}
}
```
#### .env.development (React Only)
Add in the same folder as `package.json`. Replace the port to match `SpaProxyServerUrl`:
```
PORT=44416
HTTPS=true
```
#### setupProxy.js (React Only)
Add inside `ClientApp/src/`. Replace `[IIS-HTTP-PORT]` with the IIS Express port from `Properties/launchSettings.json`, and update the `context` array with the project's API route prefixes:
```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);
};
```
### Step 8: Build and Verify
1. Build: `dotnet build`
2. Run: `dotnet run`
3. Verify the SPA proxy page appears briefly before redirecting to the frontend
4. Confirm hot-reload works in the frontend dev server
5. Test production publish: `dotnet publish` — verify the SPA output lands in `wwwroot/`
## File Summary by Framework
| File | Angular | React |
|------|---------|-------|
| Startup.cs changes | ✅ | ✅ |
| Project file changes | ✅ (dist output) | ✅ (build output) |
| launchSettings.json | ✅ | ✅ |
| package.json (start scripts) | ✅ (run-script-os) | ✅ (prestart + react-scripts) |
| angular.json | ✅ | — |
| aspnetcore-https.js | ✅ | ✅ |
| proxy.conf.js | ✅ | — |
| aspnetcore-react.js | — | ✅ |
| .env.development | — | ✅ |
| setupProxy.js (in src/) | — | ✅ |
## Troubleshooting
### SPA Proxy Page Shows But Never Redirects
The frontend dev server is not starting or is listening on the wrong port. Verify `SpaProxyLaunchCommand` is correct and `SpaProxyServerUrl` matches the actual dev server URL.
### HTTPS Certificate Errors
The dev server certificate is not trusted. Run `dotnet dev-certs https --trust` and ensure `aspnetcore-https.js` exported the certificate to the correct path.
### Port Conflicts
The backend `applicationUrl` and frontend `SpaProxyServerUrl` must use different ports. Conflicting ports cause connection refused errors.
### Static Files Not Served in Production
`SpaProxy` is development-only. For production, the `PublishRunWebpack` target in the project file must copy the SPA build output to `wwwroot/`. Verify the `DistFiles` include pattern matches the framework's output directory (`dist\` for Angular, `build\` for React).Colle ce Markdown dans ton agent ou utilise les boutons ci-dessus pour l’écrire dans ton projet.
Ce que fait Migrating Spa Services To Spa Proxy (Microsoft)
[ ] Step 1: Detect SpaServices usage and identify frontend framework
[ ] Step 2: Update Startup.cs
[ ] Step 3: Update the project file
[ ] Step 4: Update launchSettings.json
Comment utiliser Migrating Spa Services To Spa Proxy (Microsoft)
1
Copie le prompt
Un clic copie le prompt packagé (ou l'envoie à ton agent).
2
L'agent installe le skill
Il ajoute le SKILL.md et ses ressources à ton projet.
3
Activation automatique
Le skill s'active dès que le contexte correspond.
Déclencheurs pour Migrating Spa Services To Spa Proxy (Microsoft)
Dis simplement à ton agent quelque chose comme :
Applique le skill migrating-spa-services-to-spa-proxy__microsoft-upgrade-agent-plugins__plugins-upgrade-agent-extenders-upgrade-dotnet-upgrade-skills-lazy-libraries-8588cc0e98ef à cette tâche
Utilise migrating-spa-services-to-spa-proxy__microsoft-upgrade-agent-plugins__plugins-upgrade-agent-extenders-upgrade-dotnet-upgrade-skills-lazy-libraries-8588cc0e98ef pour améliorer cette implémentation
Passe en revue ce sujet avec migrating-spa-services-to-spa-proxy__microsoft-upgrade-agent-plugins__plugins-upgrade-agent-extenders-upgrade-dotnet-upgrade-skills-lazy-libraries-8588cc0e98ef
Skills liés à Migrating Spa Services To Spa Proxy (Microsoft)
accessibility
Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels, accessibility traits and hints, focus management, contrast, target size, and screen-reader support. Use when building or auditing UI for accessibility compliance, keyboard navigation, or screen-reader support.Accessibility Pass (Microsoft)
Use this skill whenever the user asks to check, review, or fix the accessibility of a PowerPoint deck, Word document, HTML page, or Markdown file, and before handing over any deck or document this agent just generated, so it does not ship with missing alt text, untitled slides, or unreadable colour contrast.Add Connector (Microsoft)
Use when adding a Power Platform connector to an Expo/React Native Power Apps mobile app and no dedicated mobile connector skill exists.