microsoft/migrating-mvc-static-files
> Migrates ASP.NET MVC static file serving and virtual path providers to ASP.NET Core conventions. Moves Content/, Scripts/, and App_Data/ folders into wwwroot/, configures UseStaticFiles middleware, and replaces VirtualPathProvider with IFileProvider and EmbeddedFileProvider. Use when upgrading MVC apps that serve CSS from ~/Content/, JS from ~/Scripts/, use VirtualPathProvider, serve embedded resources, or store files in App_Data. Also triggers for "configure static files in Core", "wwwroot migration", "embedded resource serving", or "files outside wwwroot".
npx skills add https://github.com/microsoft/upgrade-agent-plugins --skill migrating-mvc-static-files
ASP.NET Framework serves any file from the project root automatically. ASP.NET Core does not — static files are only served from wwwroot/ by default, and the UseStaticFiles middleware must be explicitly registered. This skill migrates the folder layout, configures the middleware, and replaces VirtualPathProvider with IFileProvider.
> Related skill: migrating-mvc-bundling handles bundle reference conversion. Complete bundling migration first so the files referenced by <script>/<link> tags are already in wwwroot/.
Track progress across these steps:
Migration Progress:
- [ ] Step 1: Audit existing static file locations
- [ ] Step 2: Create wwwroot structure and move files
- [ ] Step 3: Register static file middleware
- [ ] Step 4: Update path references in views and code
- [ ] Step 5: Migrate VirtualPathProvider to IFileProvider
- [ ] Step 6: Configure files outside wwwroot (if needed)
- [ ] Step 7: Remove legacy folder references
Search the project for static assets and note where they live:
Content/ — CSS files, images, fontsScripts/ — JavaScript filesApp_Data/ — data files (not served in Core by default)VirtualPathProviderCheck Web.config for any <staticContent> MIME type mappings or custom <handlers> for static files. These will need equivalent configuration in Core.
Create the wwwroot/ folder at the project root and move assets into it following ASP.NET Core conventions:
Before (ASP.NET MVC):
MyApp/Content/Site.cssimages/logo.pngScripts/jquery-3.6.0.min.jssite.jsApp_Data/data.jsonfonts/custom.woff2Web.configAfter (ASP.NET Core):
MyApp/wwwroot/css/Site.cssjs/jquery-3.6.0.min.jssite.jsimages/logo.pngfonts/custom.woff2data/data.json (only if public access needed)App_Data/data.json (keep here if private — read via file I/O)Program.csKey decisions:
Content/ → wwwroot/css/ (rename to match Core convention)Scripts/ → wwwroot/js/ (rename to match Core convention)wwwroot/images/, wwwroot/fonts/App_Data/ files that need web access → wwwroot/data/; files that should stay private → keep outside wwwroot/ and access via IWebHostEnvironment.ContentRootPathEnsure the project file includes wwwroot/ as web root content. SDK-style projects handle this automatically when the folder exists.
Add UseStaticFiles() in Program.cs. Order matters — place it before routing middleware:
Before (ASP.NET MVC — implicit, no code needed):
// Static files served automatically by IIS/ASP.NET pipeline
After (ASP.NET Core):
var app = builder.Build();
app.UseStaticFiles(); // Serves files from wwwroot/
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
To add custom MIME types (replacing Web.config <staticContent> entries):
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".webmanifest"] = "application/manifest+json";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
To configure cache headers for static files:
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=31536000");
}
});
Update all references to the old folder paths in Razor views, CSS files, and C# code.
Razor views — ~/Content/ and ~/Scripts/ references:
Before:
<link href="~/Content/Site.css" rel="stylesheet" />
<script src="~/Scripts/site.js"></script>
<img src="~/Content/images/logo.png" alt="Logo" />
After:
<link href="~/css/Site.css" rel="stylesheet" />
<script src="~/js/site.js"></script>
<img src="~/images/logo.png" alt="Logo" />
The ~/ prefix continues to work in ASP.NET Core Razor views — it resolves to wwwroot/.
CSS files — relative paths:
Before:
background-image: url('../Content/images/logo.png');
After:
background-image: url('../images/logo.png');
C# code — Server.MapPath and HostingEnvironment.MapPath:
Before:
var path = Server.MapPath("~/App_Data/data.json");
var path2 = HostingEnvironment.MapPath("~/Content/templates/report.html");
After:
// Inject IWebHostEnvironment via constructor
var path = Path.Combine(_env.ContentRootPath, "App_Data", "data.json");
var path2 = Path.Combine(_env.WebRootPath, "templates", "report.html");
Use ContentRootPath for private files outside wwwroot/ and WebRootPath for public files inside wwwroot/.
ASP.NET MVC's VirtualPathProvider has no direct replacement. ASP.NET Core uses IFileProvider for the same purpose.
Embedded resource serving:
Before (ASP.NET MVC):
// Custom VirtualPathProvider registered in Global.asax
HostingEnvironment.RegisterVirtualPathProvider(new EmbeddedResourceProvider());
After (ASP.NET Core):
var embeddedProvider = new EmbeddedFileProvider(
typeof(Program).Assembly,
"MyApp.EmbeddedAssets");
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = embeddedProvider,
RequestPath = "/embedded"
});
Mark resources as embedded in the project file:
<ItemGroup>
<EmbeddedResource Include="EmbeddedAssets/**/*" />
</ItemGroup>
Custom file system sources:
Before (ASP.NET MVC):
public class DatabaseVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath) { /* ... */ }
public override VirtualFile GetFile(string virtualPath) { /* ... */ }
}
After (ASP.NET Core):
public class DatabaseFileProvider : IFileProvider
{
public IFileInfo GetFileInfo(string subpath) { /* ... */ }
public IDirectoryContents GetDirectoryContents(string subpath) { /* ... */ }
public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
}
Register the custom provider:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new DatabaseFileProvider(connectionString),
RequestPath = "/dynamic"
});
To compose multiple providers (equivalent to chaining VirtualPathProvider instances):
var compositeProvider = new CompositeFileProvider(
builder.Environment.WebRootFileProvider,
new EmbeddedFileProvider(typeof(Program).Assembly),
new DatabaseFileProvider(connectionString));
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = compositeProvider
});
Files outside wwwroot/ are not served by default. To serve files from additional directories, add a second UseStaticFiles call with an explicit PhysicalFileProvider:
app.UseStaticFiles(); // Default: serves from wwwroot/
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "StaticAssets")),
RequestPath = "/assets"
});
Skip this step if all public assets fit under wwwroot/. Serving files from outside wwwroot/ adds complexity — prefer moving files into wwwroot/ when possible.
Search the codebase for remaining references to the old folder structure and clean them up:
Content/, Scripts/ folders (now empty after move)VirtualPathProvider registration from Global.asax or startup<staticContent> entries from Web.config (if Web.config is being removed)HostingEnvironment.MapPath and Server.MapPath callsHostingEnvironment.RegisterVirtualPathProvider callswwwroot/ folder exists with css/, js/, images/ subfolders following Core conventionsapp.UseStaticFiles() registered in Program.cs before routing middleware~/Content/ and ~/Scripts/ references updated to ~/css/ and ~/js/Server.MapPath/HostingEnvironment.MapPath replaced with IWebHostEnvironment pathsVirtualPathProvider replaced with IFileProvider/EmbeddedFileProvider (if applicable)Web.config migrated to FileExtensionContentTypeProviderContent/, Scripts/, or VirtualPathProviderTake microsoft/migrating-mvc-static-files 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.