
Description
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".
SKILL.md
ASP.NET MVC Static Files and Asset Serving Migration
Overview
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-bundlinghandles bundle reference conversion. Complete bundling migration first so the files referenced by<script>/<link>tags are already inwwwroot/.
Workflow
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
Step 1: Audit Existing Static File Locations
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)- Files in the project root or other custom folders
- Embedded resources referenced via
VirtualPathProvider
Check Web.config for any <staticContent> MIME type mappings or custom <handlers> for static files. These will need equivalent configuration in Core.
Step 2: Create wwwroot Structure and Move Files
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.css
│ └── images/
│ └── logo.png
├── Scripts/
│ ├── jquery-3.6.0.min.js
│ └── site.js
├── App_Data/
│ └── data.json
├── fonts/
│ └── custom.woff2
└── Web.config
After (ASP.NET Core):
MyApp/
├── wwwroot/
│ ├── css/
│ │ └── Site.css
│ ├── js/
│ │ ├── jquery-3.6.0.min.js
│ │ └── site.js
│ ├── images/
│ │ └── logo.png
│ ├── fonts/
│ │ └── custom.woff2
│ └── data/
│ └── data.json (only if public access needed)
├── App_Data/
│ └── data.json (keep here if private — read via file I/O)
└── Program.cs
Key decisions:
Content/→wwwroot/css/(rename to match Core convention)Scripts/→wwwroot/js/(rename to match Core convention)- Images and fonts →
wwwroot/images/,wwwroot/fonts/ App_Data/files that need web access →wwwroot/data/; files that should stay private → keep outsidewwwroot/and access viaIWebHostEnvironment.ContentRootPath
Ensure the project file includes wwwroot/ as web root content. SDK-style projects handle this automatically when the folder exists.
Step 3: Register Static File Middleware
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");
}
});
Step 4: Update Path References in Views and Code
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/.
Step 5: Migrate VirtualPathProvider to IFileProvider
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
});
Step 6: Configure Files Outside wwwroot
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.
Step 7: Remove Legacy Folder References
Search the codebase for remaining references to the old folder structure and clean them up:
- Remove
Content/,Scripts/folders (now empty after move) - Remove
VirtualPathProviderregistration fromGlobal.asaxor startup - Remove
<staticContent>entries fromWeb.config(ifWeb.configis being removed) - Remove
HostingEnvironment.MapPathandServer.MapPathcalls - Remove
HostingEnvironment.RegisterVirtualPathProvidercalls - Update any build scripts or CI pipelines that reference old paths
Success Criteria
wwwroot/folder exists withcss/,js/,images/subfolders following Core conventionsapp.UseStaticFiles()registered inProgram.csbefore routing middleware- All
~/Content/and~/Scripts/references updated to~/css/and~/js/ Server.MapPath/HostingEnvironment.MapPathreplaced withIWebHostEnvironmentpathsVirtualPathProviderreplaced withIFileProvider/EmbeddedFileProvider(if applicable)- Custom MIME types from
Web.configmigrated toFileExtensionContentTypeProvider - No remaining references to
Content/,Scripts/, orVirtualPathProvider - Project builds without errors
More skills from the upgrade-agent-plugins repository
View all 19 skillsconverting-to-cpm
migrate .NET projects to Central Package Management
Jul 18.NETMicrosoftMigrationcreating-winforms-custom-controls
create custom WinForms controls
Jul 3.NETWindowsWinUImanaging-winforms-high-dpi-layout
design high-DPI responsive WinForms layouts
Jul 18DesignDesktopUI ComponentsWindowsmanaging-winforms-mvvm
implement MVVM pattern in WinForms
Jul 18.NETWindowsWinUImigrating-aspnet-framework-to-core
migrate ASP.NET Framework to Core
Jul 3.NETASP.NET CoreBackendMigrationmigrating-documentdb-to-cosmos
migrate DocumentDB to Cosmos DB
Jul 3AzureCosmos DBDatabaseMigration
More from Microsoft
View publisherplaywright-trace
inspect Playwright trace files
playwright
Apr 6DebuggingPlaywrightTestingrushstack-best-practices
manage Rush monorepos with best practices
rushstack
Apr 6EngineeringLocal DevelopmentMicrosoftProject Management +1azure-ai-agents-persistent-dotnet
build AI agents with Azure .NET SDK
skills
Jul 3.NETAgentsAzureLLMazure-ai-anomalydetector-java
build anomaly detection applications with Java
skills
May 13AnalyticsAzureData AnalysisJava +2azure-ai-contentsafety-java
build content moderation applications with Azure AI
skills
Jul 7AI InfrastructureAzureJavaSecurityazure-ai-contentsafety-py
detect harmful content with Azure AI Content Safety
skills
Jul 18AzureComplianceLLMMicrosoft +2