This solution contains four projects that work together:
src/ModernApi: a .NET 10 Web API that serves a paged, filterable people directory backed by deterministic in-memory seed data.src/LpListSpa: a React app that renders the shared people directory with server-side paging, filtering, editing, and SignalR presence.src/LegacyHost.WebForms: a .NET Framework Web Forms app that loads the built React bundle and shows the API data inside the legacy page.src/ModernHost.Mvc: a .NET 10 ASP.NET Core MVC app that loads the same built React bundle and shows the same API data and SignalR presence in a Razor host.
For a generated side-by-side capture that shows both hosts editing the same person and surfacing the live SignalR presence warning, see docs/mixed-host-side-by-side-presence.gif.
From the solution root, run either of these:
.\run-all.ps1or:
run-all.cmdThe launcher does all of the following:
- builds
WebFormReactMVC.slnx - starts
ModernApionhttps://localhost:7114 - starts
ModernHost.Mvconhttps://localhost:7184 - starts
LegacyHost.WebFormswith IIS Express when available - opens the MVC and legacy host pages in your browser
Optional switches:
-SkipBuild: skip the build and reuse the current outputs-SkipLegacyHost: start onlyModernApiandModernHost.Mvc-NoBrowser: do not automatically open browser tabs
Examples:
.\run-all.ps1 -SkipBuild
.\run-all.ps1 -SkipLegacyHost
.\run-all.ps1 -SkipBuild -NoBrowserIf IIS Express is not installed, the script still starts the API and MVC host and prints the manual fallback for the legacy host.
The React component is not hard-coded into either host project.
Instead, the flow is:
LpListSpabuilds a React bundle.- That bundle is copied into
src/LegacyHost.WebForms/ClientAppandsrc/ModernHost.Mvc/wwwroot/people-app. src/LegacyHost.WebForms/Default.aspxandsrc/ModernHost.Mvc/Views/Home/Index.cshtmlload that bundle.- The bundle calls
ModernApiathttps://localhost:7114/api/people. - The API returns a paged envelope of people plus metadata.
- The React component renders the current page of results inside the Web Forms page and the MVC page.
SignalR is also used for live editing presence:
- Each browser tab connects to
ModernApiover SignalR. - When a user opens an edit dialog, that tab announces which person is being edited.
- Other open pages immediately see who is currently editing which person.
- When the dialog is closed or saved, the editing presence is cleared.
Install all of the following before trying to run the full end-to-end flow:
- .NET 10 SDK
- Node.js and npm
- Visual Studio 2022 or later with ASP.NET and web development support
- .NET Framework 4.8 targeting pack
Important:
ModernApiandModernHost.Mvccan be run from the CLI withdotnet run.LegacyHost.WebFormsis a classic ASP.NET Web Forms project. The included launcher starts it with IIS Express when IIS Express is installed.- Both host apps are configured to call
https://localhost:7114by default.
Before the first run, trust the local .NET development certificate if needed:
dotnet dev-certs https --trustFrom the solution root, run:
dotnet build WebFormReactMVC.slnxWhat this does:
- builds
ModernApi - builds
ModernHost.Mvc - builds
LpListSpa - creates the React bundle in
src/LpListSpa/dist - copies the built files into
src/LegacyHost.WebForms/ClientApp - copies the built files into
src/ModernHost.Mvc/wwwroot/people-app
After a successful build, these files should exist:
src/LegacyHost.WebForms/ClientApp/people-app.jssrc/LegacyHost.WebForms/ClientApp/people-app.csssrc/ModernHost.Mvc/wwwroot/people-app/people-app.jssrc/ModernHost.Mvc/wwwroot/people-app/people-app.css
If those files do not exist, the React component cannot appear inside the corresponding host page.
Use these only if you do not want to use run-all.ps1.
From the solution root:
dotnet run --project src/ModernApi --launch-profile httpsWait until you see:
Now listening on: https://localhost:7114
You can verify the API by opening this URL in a browser or calling it from PowerShell:
https://localhost:7114/api/people?page=1&pageSize=25
Expected result: JSON containing a paged envelope with items, page, pageSize, totalCount, totalPages, hasPreviousPage, hasNextPage, and appliedFilters.
From the solution root:
dotnet run --project src/ModernHost.Mvc/ModernHost.Mvc.csproj --launch-profile httpsWait until you see:
Now listening on: https://localhost:7184
Then open:
https://localhost:7184
When the MVC host page opens, it will:
- load
people-app.jsfrom/people-app - mount the shared React component
- call
ModernApi - render a paged, filterable directory backed by
/api/people
Open:
WebFormReactMVC.slnx
Make sure LegacyHost.WebForms is the startup project if you want the browser to open directly into the legacy app.
In Visual Studio:
- Right-click
LegacyHost.WebForms - Choose
Set as Startup Project - Press
F5or clickIIS Express
Visual Studio will launch the Web Forms site.
When Default.aspx opens, the page will:
- load
people-app.js - mount the shared React component into the legacy page
- call
ModernApi - render a paged, filterable directory backed by
/api/people
If everything is working, you will see the people directory rendered inside the Web Forms page.
You can run both hosts at the same time to compare behavior and verify shared SignalR presence across them:
https://localhost:7184http://localhost:63755/Default.aspx
The fastest dependable path is now:
.\run-all.ps1If you want to skip the legacy host and only run the API plus MVC host:
.\run-all.ps1 -SkipLegacyHostIf you edit files under src/LpListSpa/src, you must rebuild so the updated bundle is copied into both host projects.
Use either of these:
dotnet build src/LpListSpa/LpListSpa.csprojor:
dotnet build WebFormReactMVC.slnxEither option rebuilds the SPA bundle and refreshes:
src/LegacyHost.WebForms/ClientAppsrc/ModernHost.Mvc/wwwroot/people-app
After that, refresh whichever host pages are currently running.
If you want to work on the React UI outside either host:
cd src/LpListSpa
npm run devThen open the Vite URL shown in the terminal, usually:
http://localhost:5173
In this mode, Vite proxies /api and /hubs requests to https://localhost:7114, so ModernApi must still be running.
This is useful for frontend iteration, but it is not the host-integrated scenario.
Playwright coverage now includes:
LegacyHost.WebFormsModernHost.Mvc- paged API contract coverage for
ModernApi - a mixed-host scenario that verifies shared SignalR editing presence across both hosts
From the tests folder:
npm install
npm run install:browsers
npm testFor targeted commands and environment overrides, see tests/README.md.
To verify that other users can see who is editing which person across either host:
- Start
ModernApi - Start
ModernHost.Mvc,LegacyHost.WebForms, or both - Open any two pages or tabs from this set:
https://localhost:7184http://localhost:63755/Default.aspx
- Use the search box or pager if needed to locate a person, then click
Edit - In the other page, look at the
Who is editing right nowsection and the person card badge
Expected behavior:
- each page gets its own default display name
- opening the dialog shows that user as currently editing the selected person
- the presence appears in the same host and in the other host if both are running
- closing or saving the dialog removes that presence from the other page
The display name is stored per browser tab session, so two tabs can appear as different users during local testing.
The Web Forms host reads the API base URL from:
src/LegacyHost.WebForms/Web.config
Current setting:
<add key="ModernApiBaseUrl" value="https://localhost:7114" />The MVC host reads the API base URL from:
src/ModernHost.Mvc/appsettings.Development.jsonsrc/ModernHost.Mvc/appsettings.json
Current development setting:
"ModernApiBaseUrl": "https://localhost:7114"Both hosts should point to the same ModernApi URL when you run them side by side.
The API launch URL is defined in:
src/ModernApi/Properties/launchSettings.json
Current HTTPS URL:
https://localhost:7114
The MVC host launch URLs are defined in:
src/ModernHost.Mvc/Properties/launchSettings.json
Current HTTPS URL:
https://localhost:7184
Check all of the following:
- If you are testing Web Forms,
src/LegacyHost.WebForms/ClientApp/people-app.jsandpeople-app.cssexist - If you are testing MVC,
src/ModernHost.Mvc/wwwroot/people-app/people-app.jsandpeople-app.cssexist - You rebuilt after changing the React app
- Browser dev tools do not show a 404 for
/ClientApp/people-app.jsor/people-app/people-app.js
Check all of the following:
ModernApiis still runninghttps://localhost:7114/api/people?page=1&pageSize=25returns JSON- The active host configuration still points to
https://localhost:7114src/LegacyHost.WebForms/Web.configfor Web Formssrc/ModernHost.Mvc/appsettings.Development.jsonfor MVC
- The browser does not show a certificate warning or blocked HTTPS request
Run:
dotnet dev-certs https --trustThen restart the API and reload the page.
Check all of the following:
- IIS Express exists at
C:\Program Files\IIS Express\iisexpress.exeor setPLAYWRIGHT_IIS_EXPRESS_PATH - If you already opened the solution in Visual Studio, the
.vs\<solution>\config\applicationhost.configfile still exists - If IIS Express is available but the launcher still cannot start the legacy host, open
WebFormReactMVC.slnxin Visual Studio and runLegacyHost.WebFormswith IIS Express once
This repository was verified with:
dotnet build WebFormReactMVC.slnxdotnet run --project src/ModernApi --launch-profile httpsdotnet run --project src/ModernHost.Mvc/ModernHost.Mvc.csproj --launch-profile httpscd tests && npm run typecheck
The API returned the expected in-memory list of five people, the build successfully produced the React assets consumed by both hosts, and the Playwright page coverage now includes legacy-host, MVC-host, and mixed-host scenarios.
