| name | webforms-to-angular |
|---|---|
| description | Migrate ASP.NET Web Forms (2008 era, .NET Framework 2.0/3.5/4.x) projects to a modern stack of Angular (standalone components, signals) frontend + ASP.NET Core .NET 8 backend. Use this skill whenever the user mentions migrating, modernizing, or porting Web Forms, .aspx, .ascx, code-behind, GridView, Repeater, ViewState, postback, UpdatePanel, FormsAuthentication, or any legacy ASP.NET concept — even if they don't explicitly say "Angular". Also use when the user wants to analyze a Web Forms project to plan a migration, extract business logic from code-behind into a clean API layer, or generate Angular components that replicate the behavior of an .aspx page. The skill assumes business logic is mixed between code-behind and a service layer (typical of 2008 projects) and produces code that is idiomatic for both Angular 17+ and .NET 8. |
It guides a per-page migration of an ASP.NET Web Forms application into:
- Frontend: Angular 17+ with standalone components, signals, typed reactive forms, and HTTP client.
- Backend: ASP.NET Core .NET 8 with Minimal APIs (or Controllers when the user prefers), clean separation between transport (HTTP), application logic, and data access.
The skill is page-driven: the unit of work is one .aspx (or .ascx) plus its .aspx.cs code-behind. Trying to migrate a whole solution in one shot fails — always work page-by-page after an inventory pass.
Every migration request falls into one of four phases. Identify which one the user is in before reading any reference file, and then load only the references you need.
If the user is starting a new migration and you don't have a clear picture of the project, run an inventory pass first. The goal is to know what you're dealing with before touching any single page.
Use scripts/inventory.py against the solution root. It walks the project and emits a JSON report with:
- Every
.aspx/.ascx/.master - Server controls used per page (GridView, Repeater, FormView, custom user controls, third-party like Telerik/DevExpress/Infragistics)
- Code-behind size (rough complexity signal)
- Detected cross-cutting concerns (FormsAuthentication, Session, Cache, UpdatePanel, AJAX ScriptManager)
- Database access patterns visible in code-behind (
SqlCommand, EF, Linq2SQL, custom DAL)
Present the inventory to the user and agree on a migration order before generating anything. Good defaults: leaf pages first, pages with the least cross-cutting concerns first, dashboards/reports last.
User says something like "migrate OrdersGrid.aspx" or "what's in this page".
- Read the
.aspxmarkup and the.aspx.cscode-behind. If there's a.designer.cs, read it too — it tells you exactly which controls the code-behind references. - Run
scripts/analyze_aspx.py <path>to get a structured JSON of controls + event handlers, or do the parse inline if the page is small. - Open
references/control-mapping.mdto translate each control into its Angular equivalent. - If the code-behind has business logic mixed in (it usually does — the user confirmed this is their situation), open
references/codebehind-extraction.mdand identify what belongs in the API vs. what was purely presentational glue. - If the page uses
ViewState,IsPostBack, dynamic control creation, orUpdatePanel, openreferences/viewstate-and-postback.md— these need explicit translation, not naive mapping. - If the page uses
FormsAuthentication,[Authorize]attributes, or readsUser.Identity, openreferences/auth-and-session.md.
Produce an analysis summary before writing any code. Format:
## Page: OrdersGrid.aspx
### Controls to migrate
- asp:GridView "gvOrders" → <table> with *ngFor + signal-based pagination
- asp:DropDownList "ddlStatus" → <select> bound to FormControl
- asp:Button "btnExport" → (click) handler → calls service.export()
### Server-side behavior
- Page_Load: loads orders from OrderService.GetByUser(currentUserId)
- gvOrders_RowCommand: handles "Approve" / "Reject" → OrderService.UpdateStatus
- btnExport_Click: streams CSV via Response.Write
### To extract into API
- GET /api/orders?status={status} ← replaces Page_Load query
- PATCH /api/orders/{id}/status ← replaces RowCommand
- GET /api/orders/export?status={status} ← replaces btnExport_Click (returns file)
### Concerns to flag
- ViewState is used to remember the current filter across postbacks → in Angular this becomes a signal in the component (or a route query param if shareable)
- FormsAuthentication ticket → migrate to JWT or cookie auth; see auth-and-session.md
Then ask the user: "Does this analysis look right? Once you confirm, I'll generate the API endpoints, then the Angular component."
Only after the analysis is confirmed. Open references/api-design-net8.md.
Generate, in order:
- DTOs / contracts — one record per request and response. Keep them flat; don't ship EF entities over the wire.
- Endpoint(s) — Minimal API by default. Use the template in
assets/templates/minimal-api-endpoint.cs.tmpl. Group related endpoints withMapGroup. - Application service — pure C# class with the business logic that was previously mixed into code-behind. No
HttpContext, noPage, noSession. Constructor-inject what it needs. - Wiring — show the
Program.cslines the user must add (DI registrations, theMapGroupcall). Don't rewrite the wholeProgram.cs; just emit the diff.
Always emit a small integration note describing how this endpoint will be consumed by the Angular service generated in Phase 3.
Open references/angular-patterns.md.
Generate three files using the templates in assets/templates/:
<feature>.service.ts— typed HTTP service. One method per endpoint from Phase 2. ReturnsObservable<T>or signal — match the project's style if you can detect it.<feature>.component.ts— standalone component, signals for state, typed reactive form for inputs.<feature>.component.html— template that replicates the original page's structure.
Default style choices when the user hasn't specified:
- Standalone components (no NgModule).
- Signals for local component state. RxJS only for HTTP and stream-like concerns.
- Typed reactive forms (
FormGroup<T>withFormControl<T>), not template-driven. @if/@forcontrol flow (Angular 17+), not*ngIf/*ngFor.- Material or plain HTML — ask once at the start of the migration and remember the answer for the rest.
Never migrate one-to-one. Web Forms encodes server-driven UI patterns (postback, ViewState, server-side eventing) that have no direct Angular equivalent. Your job is to translate the user-visible behavior, not the mechanism. A GridView with sorting and paging becomes a client-side table with column sort and a paginator, served by a paginated API — not a custom directive that pretends to be a GridView.
Code-behind is not application logic. When you read .aspx.cs, mentally bucket every line into one of: (a) UI plumbing that disappears (binding controls, reading TextBox.Text), (b) presentational logic that moves to Angular (when to disable a button, when to show an error), (c) business logic that moves to a .NET 8 application service. The third bucket is what becomes API endpoints.
ViewState is not state. It's a workaround for HTTP being stateless plus Web Forms pretending it isn't. In Angular, state is either local (signal/component) or shared (service/signal store/route params). Never try to "preserve" ViewState — figure out what it was preserving and put that thing in the right place.
Don't recreate WebMethod-style RPC. It's tempting to map every server event handler to a POST endpoint named after the handler. Resist this. Design REST-ish endpoints around resources, not around button clicks. btnApprove_Click becomes PATCH /orders/{id}/status, not POST /api/btnApprove.
Keep the legacy app running until the new one is verified. Suggest a strangler-fig approach: the .NET 8 API can be deployed alongside the legacy app, and Angular pages can replace .aspx pages one route at a time behind a reverse proxy. Don't push for a big-bang cutover.
Ask the user before:
- Choosing a UI component library (Material vs. PrimeNG vs. plain) — once per migration, then remember.
- Deciding the API style when ambiguous (Minimal API vs. Controllers, REST vs. tRPC-style RPC).
- Picking the auth strategy if the legacy app uses something unusual (custom auth modules, federated SSO).
- Touching the database schema — never assume you can change it; many legacy apps share a DB with other systems.
Generate without asking:
- File scaffolding once the analysis is confirmed.
- Standard refactors (extracting a service from code-behind, splitting a DTO from an entity).
- Naming conventions (kebab-case for Angular files, PascalCase for C#,
I-prefixed interfaces only if the user's existing .NET code uses that style).
| If the page or task involves... | Read |
|---|---|
| Any server control (GridView, Repeater, etc.) | references/control-mapping.md |
| Untangling logic from code-behind | references/codebehind-extraction.md |
| ViewState, IsPostBack, UpdatePanel, dynamic controls | references/viewstate-and-postback.md |
| Designing the .NET 8 endpoints | references/api-design-net8.md |
| Writing the Angular component / service | references/angular-patterns.md |
| FormsAuthentication, roles, Session-based auth | references/auth-and-session.md |
Read only what's relevant. A static "About" page doesn't need viewstate-and-postback.md.
The files in assets/templates/ are starting points, not final outputs. Customize them to match the user's project style — namespaces, file headers, lint rules. Use them when you need a consistent scaffold, ignore them when the user has shown you a different convention earlier in the conversation.
assets/examples/ contains a worked end-to-end migration of a small CustomerList.aspx. Read these when you need a concrete reference for what "done" looks like.