microsoft/migrating-aspnet-framework-to-core
> Orchestrates migration of ASP.NET Framework (System.Web) MVC and WebAPI projects to ASP.NET Core. Covers only old .NET Framework web projects — not applicable to ASP.NET Core or modern .NET web projects (those are already on the target stack). Defines the ordered phase sequence (project file, host, config, DI, controllers, middleware, auth, views, cleanup), satellite skill dispatch, and migration unit breakdown for both in-place and side-by-side modes. Use when executing a task that upgrades a project with System.Web dependencies, HttpModules, HttpHandlers, MVC controllers, WebAPI controllers, or Global.asax. Also triggers for "migrate ASP.NET to Core", "upgrade MVC project", "convert WebAPI to ASP.NET Core". Not applicable to class libraries unless they directly reference System.Web.
npx skills add https://github.com/microsoft/upgrade-agent-plugins --skill migrating-aspnet-framework-to-core
This skill defines how to execute a web project migration task:
It does not define which projects to migrate or in what order across the solution —
that is the strategy's responsibility.
Do not mention internal step names (like "Phase 5") in chat. Describe the work
in plain language: "migrating controllers", "setting up configuration", etc.
Two modes depending on the Project Approach upgrade option:
| Project Approach | Mode | Guidance |
|-----------------|------|----------|
| In-place rewrite | In-Place | Execute all steps sequentially within one task |
| Side-by-side | Side-by-Side | Load side-by-side.md for scaffold/migrate task boundaries |
Determine mode from scenario-instructions.md (Upgrade Options > Project Approach) before proceeding.
This migration touches many complex feature areas. Each has a dedicated satellite skill
with depth this orchestrator does not repeat. Load satellites just before the phase
that needs them — not all upfront.
| Assessment Signal | Satellite Skill | Load Before |
|-------------------|----------------|-------------|
| UsesHttpModules, UsesHttpHandlers, UsesGlobalAsax | migrating-global-asax, migrating-mvc-http-pipeline | Middleware migration |
| UsesHttpContextCurrent, UsesHttpServerUtility | migrating-mvc-httpcontext | DI container setup |
| UsesFormsAuth, UsesMembership, UsesWindowsAuth, UsesOwinAuth, UsesOAuthMiddleware | migrating-mvc-authentication | Authentication migration |
| UsesOwin, UsesKatana, UsesAppBuilder | migrating-owin-to-aspnet-core | Middleware migration |
| UsesSession, UsesTempData, UsesApplicationState | migrating-mvc-session-state | DI container setup |
| UsesCustomDependencyResolver, UsesAutofac, UsesUnity, UsesNinject, UsesCastleWindsor | migrating-mvc-dependency-injection | DI container setup |
| UsesAttributeRouting, UsesRouteConstraints, UsesAreaRouting | migrating-mvc-routing | Controller migration |
| UsesWebApiControllers, UsesHttpResponseMessage, UsesContentNegotiation, UsesCustomFormatters | migrating-mvc-controllers, migrating-mvc-content-negotiation | Controller migration |
| UsesMvcControllers, UsesChildActions, UsesHtmlHelpers | migrating-mvc-controllers | Controller migration |
| UsesCustomFilters, UsesOutputCache, UsesHandleError | migrating-mvc-filters | Controller migration |
| UsesCustomModelBinders, UsesFromUri, UsesValueProviders | migrating-mvc-model-binding | Controller migration |
| UsesMvcViews, UsesBundling, UsesHtmlHelpers, UsesChildActions | migrating-mvc-razor-views | Views migration |
for this project and note them. Do not load them yet.
budget matters; load only what the current step requires.
general knowledge, and flag areas requiring manual review.
starting that step.
These steps apply to both modes. In-place executes them sequentially.
Side-by-side splits them across tasks — see side-by-side.md.
Before any changes, record:
Endpoint inventory (becomes acceptance checklist for final verification):
Pipeline inventory (drives middleware migration):
HttpModule registrations and their pipeline event hooks, in orderHttpHandler and IHttpAsyncHandler registrationsGlobal.asax event handlers in useFeature inventory (determines satellite loading):
HttpContext.Current / ClaimsPrincipal.Current static access patternsGate: Baseline document exists. Satellite list noted. No proceeding until complete —
this document is the acceptance oracle for final verification.
.csproj format with SDK-style format<TargetFramework> to target versionpackages.config — migrate all references to <PackageReference>Microsoft.WebApplication.targets, etc.)System.Web assembly references from project fileGate: Project loads in IDE without errors. No compilation required yet.
Program.cs with minimal WebApplication host — no features, stub onlyapp.MapGet("/health", () => "ok") as a smoke test endpointdotnet build succeeds, dotnet run starts, stub endpoint returns 200Gate: App starts and responds. This is the "green field confirmed" gate.
Do not add any features until this is green — a broken host wastes all subsequent work.
Web.config <appSettings> → appsettings.jsonWeb.config <connectionStrings> → appsettings.json connection strings sectionIConfiguration in Program.csConfigurationManager.AppSettings["key"] calls with IConfiguration["key"]ConfigurationManager.ConnectionStrings["name"] with IConfiguration.GetConnectionString("name")Web.Debug.config) → appsettings.Development.jsonGate: All configuration keys accessible via IConfiguration.
No ConfigurationManager references remain in non-legacy code.
Note: Do not migrate authentication configuration here — that belongs to the
authentication migration step.
Connection strings are config, auth settings are not.
> If assessment signals include UsesCustomConfigSections, UsesEncryptedConfig,
> or UsesConfigTransforms beyond simple appSettings → flag for manual review.
> Custom config section types have no direct equivalent and require IOptions<T> redesign.
> Load satellites before this phase:
> - migrating-mvc-dependency-injection if any of: UsesAutofac, UsesUnity, UsesNinject,
> UsesCastleWindsor, UsesCustomDependencyResolver
> - migrating-mvc-httpcontext if: UsesHttpContextCurrent, UsesHttpServerUtility
> - migrating-mvc-session-state if: UsesSession, UsesApplicationState
Program.cs or extension methodsDependencyResolver.SetResolver (MVC) or config.DependencyResolver (WebAPI)with builder.Services registrations
DbContext, repositories, application servicesIHttpContextAccessor registration if HttpContext.Current usage was found during baseline capturebuilder.Services.AddSession())System.Web dependencies — stub them withNotImplementedException and a TODO comment, resolve during controller migration
Lifetime mapping:
| Old | New |
|-----|-----|
| Per-request / InstancePerRequest | Scoped |
| Singleton | Singleton |
| Transient / InstancePerDependency | Transient |
Gate: Application starts and DI container resolves without errors.
All services registered (stubs acceptable for System.Web-dependent ones).
> Load satellites before this phase:
> - migrating-mvc-routing if: UsesAttributeRouting, UsesRouteConstraints, UsesAreaRouting
> - migrating-mvc-controllers if: UsesWebApiControllers, UsesHttpResponseMessage
> - migrating-mvc-controllers if: UsesMvcControllers, UsesChildActions
> - migrating-mvc-filters if: UsesCustomFilters, UsesOutputCache, UsesHandleError
> - migrating-mvc-model-binding if: UsesCustomModelBinders, UsesFromUri
> In Side-by-Side mode: This phase is the repeating migration unit.
> Each unit = one controller group (by feature area) + associated filters + models.
> Complete and validate each unit before starting the next.
> See Mode Reference section for unit ordering guidance.
>
> Controller triage (before creating units): Read each controller file to
> assess complexity — constructor dependencies, auth requirements, action count,
> use of complex features (child actions, custom filters, model binders), and
> any other signals that indicate migration difficulty. Group by feature area
> (folders, naming, areas). Order: simplest first, auth-dependent last.
> Complex controllers with many dependencies should be their own unit.
>
> Per-unit dependency discovery (when starting each unit): Use
> get_code_dependencies on the controller(s) in the current unit to discover
> explicit code dependencies — services, models, views, packages. Then also
> check for implicit dependencies not visible in the code graph:
> - Review baseline capture: which HTTP modules, handlers, or Global.asax
> events affect this controller's endpoints?
> - Check for HttpContext.Current, ConfigurationManager, or static helper
> usage that won't work in Core without explicit registration
> - Check RouteConfig.cs / WebApiConfig.cs for non-attribute routes that
> serve this controller
> - Check FilterConfig.cs for global filters this controller depends on
>
> Verify each dependency is ready in the new project:
> - DI: are the controller's injected services registered? Replace stubs if needed.
> - Config: are the config keys the controller reads present in appsettings.json?
> - References: can the new project reference the class libraries this controller uses?
> - Routes: is app.MapControllers() in the pipeline? Are non-attribute routes configured?
> - Pipeline: are HTTP modules this controller depends on replicated as middleware?
>
> Fix any gaps before porting the controller code. Document in task.md.
For each controller:
System.Web.Mvc or System.Web.Http to Microsoft.AspNetCore.MvcController base classApiController to ControllerBase[ApiController] attribute if pure API (no views) — understand its behaviorchanges before applying (auto-400, binding inference)
migrating-mvc-routing satellite for combining rulesmigrating-mvc-controllers for WebAPI specifics[FromUri] → [FromQuery]/[FromRoute])migrating-mvc-filters satelliteAreaRegistration classes — keep area folder structure, registration is now automaticGate (per unit in side-by-side): Unit builds, all endpoints in unit return expected
status codes per baseline, unit tests pass.
Gate (in-place): All controllers migrated, solution builds, all endpoints respond
per baseline.
> Load satellites before this phase:
> - migrating-global-asax, migrating-mvc-http-pipeline if: UsesHttpModules, UsesHttpHandlers, UsesGlobalAsax
> - migrating-owin-to-aspnet-core if: UsesOwin, UsesKatana
Pipeline ordering is the critical concern here. Reconstruct from baseline inventory.
High-level mapping reference:
| Old construct | Core equivalent |
|---------------|----------------|
| IHttpModule.BeginRequest | Middleware registered before next() |
| IHttpModule.EndRequest | Middleware registered after next() |
| IHttpModule.AuthenticateRequest | Auth middleware — position relative to UseAuthentication() |
| IHttpHandler | Minimal API endpoint or terminal middleware |
| IHttpAsyncHandler | Async terminal middleware |
| Global.asax Application_Start | Program.cs startup code |
| Global.asax Application_End | IHostApplicationLifetime.ApplicationStopping |
| Global.asax Application_Error | Exception handling middleware |
| DelegatingHandler (WebAPI) | Middleware — position in pipeline must match original handler order |
> For anything beyond basic module-to-middleware mapping, consult migrating-mvc-http-pipeline.
> Pipeline ordering errors produce subtle bugs that are difficult to diagnose.
Gate: Application pipeline behaves equivalently to baseline for
non-authenticated requests. All custom middleware registered in correct order.
> Load satellite before this phase:
> - migrating-mvc-authentication — always load for this phase when any auth is present.
> Auth is security-critical and has multiple distinct migration paths.
> Do not proceed based on general knowledge alone.
> Execute this step after the pipeline is stable (middleware migration complete).
> Auth is the hardest to debug when the pipeline is uncertain.
High-level path selection (satellite provides full detail for each):
| Detected auth mechanism | Migration path |
|------------------------|----------------|
| FormsAuthentication | Cookie authentication middleware |
| SqlMembership / SimpleMembership | ASP.NET Core Identity (schema migration required) |
| Custom MembershipProvider | Custom IUserStore<T> or IAuthenticationHandler |
| Windows Authentication | Negotiate middleware |
| OWIN OAuth / token server | IdentityServer / Duende / OpenIddict |
| Custom IPrincipal / claims | IClaimsTransformation |
| Web.config <authorization> rules | Policy-based authorization |
Gate: Authenticated and unauthenticated request flows match baseline.
Run the full auth rules inventory from baseline capture as acceptance checklist.
> Load satellite before this phase:
> - migrating-mvc-razor-views if: UsesMvcViews, UsesBundling, UsesHtmlHelpers,
> UsesChildActions, UsesDisplayTemplates, UsesEditorTemplates
High-level checklist (satellite provides full detail):
@Scripts.Render / @Styles.Render calls — no built-in equivalentHtmlHelper calls with Tag Helpers (Html.ActionLink → <a asp-*>, etc.)Html.Action()) to View Components@helper Razor helpers to partial views or Tag HelpersContent/ and Scripts/ to wwwroot/app.UseStaticFiles() to Program.cs_ViewImports.cshtml with Tag Helper namespace imports_ViewStart.cshtml layout reference is correctGate: All views render without errors. Static assets load. No @Scripts.Render
or @Styles.Render references remain.
#if NETFRAMEWORK conditional blocksSystem.Web references if any remain (there should be none)WebApiConfig.cs, RouteConfig.cs, FilterConfig.cs, BundleConfig.csfrom old project's code that was copied to new project
packages.config if not already done> Do NOT delete the old Framework project. In side-by-side mode, the old
> project stays in the solution. Physical removal is a post-upgrade step for
> the user. In in-place mode, this section cleans up the converted project.
Verify against baseline:
System.Web references remain (dotnet list package confirms)If the task scope includes class libraries that reference System.Web:
System.Web API surface with abstractions injectable via DIHttpContext.Current in a library — load migrating-mvc-httpcontext satelliteHttpServerUtility usage — no direct equivalent, method-by-method replacement neededHttpContext.Current.User) — requires IHttpContextAccessor threading careAll migration steps execute sequentially within a single project upgrade task.
No task boundaries between steps.
Load side-by-side.md for the complete scaffold/migrate
task structure, scaffold checklist, and controller migration subtask ordering.
Old project is not deleted — removal is a post-upgrade step for the user.
Take microsoft/migrating-aspnet-framework-to-core from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.