I started developing my website in 2012, and it has gone through so many changes. The first version was written in PHP. Then I eventually moved to ASP.NET MVC 3.0. The UI changed a couple of times, and the major version already exceeded number 5.

The last major iteration was switching from classic .NET Framework to .NET Core 2.0 and ASP.NET Core as well.

With those changes, you can get many benefits. Some of them you will find on the official introduction page. You can find many articles regarding ASP.NET Core migration like this. However, they don't cover all cases, that's why I decided to share my own experience.


Project structure

No significant changes here, Controllers, Views, Models, Areas folders stay in its place. The main difference is Content folder which renamed to wwwroot and represents a home for static content (images, styles, javascript, etc.).

Dependency injection

Another great feature is built-in dependency injection capabilities in ASP.NET Core. You don't need Ninject or Unity or any other DI frameworks. Open your Startup file and place all of your dependencies inside ConfigureServices(IServiceCollection services) method. Use AddTransient, AddSingleton or other methods of interface IServiceCollection to register your services.

Client-side packages

Nuget isn't a client-side package provider for ASP.NET Core applications. If you want to add jQuery to your project, you should consider using different methods.

Visual Studio today supports Bower, NPM and recently LibMan (you can even use other solutions, which is not natively integrated with IDE).

Web.config file

Good news, ASP.NET Core isn't tied with IIS anymore. This means that web.config is no longer required (if you decide to host your application on IIS, web.config will be auto-generated). Where should you move all data from config file? Well, consider to spread it across different places:

Web.config section New location
appSettings appsettings.json file
system.web\pages\namespaces _ViewImports.cshtml file
system.webServer\httpErrors app.UseStatusCodePagesWithReExecute("/Error/Index", "?statusCode={0}"), inside Configure method of Startup class
system.webServer\staticContent app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = CreateContentTypeProvider() }), inside Configure method of Startup class
Remaining data most-likely will go to Startup.cs file

Global.asax file

This long-living file is also going away with the new framework. Most data will move to Startup.cs file.

Custom model metadata provider

I used custom model metadata provider to override how MVC handles the generation of metadata for my models. If this applies to you, read the text below:


Before in your Global.asax Application_Start method you would do ModelMetadataProviders.Current = new YourMetadataProvider().

Now in your Startup class you will add services.AddSingleton<IModelMetadataProvider, YourMetadataProvider>() inside ConfigureServices method.


Before you would use DataAnnotationsModelMetadataProvider as a base class for your metadata provider, and you would override CreateMetadata method.

Now you will use DefaultModelMetadataProvider as a base class for your metadata provider, and you will override CreateModelMetadata method.

Client-side validation

If you develop HtmlHelper extensions, you might face a situation when you need to get client-side validation attributes.

With classic ASP.NET you can easily do this with GetUnobtrusiveValidationAttributes method:

tagBuilder.MergeAttributes<string, object>(htmlHelper.GetUnobtrusiveValidationAttributes(expression));

A lot of things have changed. Now to accomplish the same goal please use the following code:

var modelExplorer = ExpressionMetadataProvider.FromStringExpression(expression, htmlHelper.ViewContext.ViewData, htmlHelper.MetadataProvider);
var validator = htmlHelper.ViewContext.HttpContext.RequestServices.GetService<ValidationHtmlAttributeProvider>();
validator?.AddAndTrackValidationAttributes(htmlHelper.ViewContext, modelExplorer, expression, tagBuilder.Attributes);


Only registration has changed in routing.

Before you would do RouteConfig.RegisterRoutes(RouteTable.Routes) inside Global.asax Application_Start method.

Now you will add app.UseMvc(RegisterRoutes) inside Configure method of Startup class.


To register your application areas:

Before you would do AreaRegistration.RegisterAllAreas() inside Global.asax Application_Start method.

Now you will add routes.MapAreaRoute("your-route-name", "your-area-name", "your-area-template") to your routing configuration for each area.

Small rocks

Here I would like to share some small changes which I noticed during migration.

Before After
ActionResult renamed to IActionResult
UrlHelper renamed to IUrlHelper
MvcHtmlString renamed to IHtmlContent
HttpPostedFile renamed to IFormFile
Controller.HttpNotFound() renamed to Controller.NotFound()
HttpRequestBase renamed to HttpRequest
ActionDescriptor.ActionName renamed to ActionDescriptor.DisplayName
HttpPostedFile.InputStream changed to IFormFile.OpenReadStream()
Request[] changed to Request.Form[]
Request.Files.Get() changed to Request.Form.Files[]
Request.HttpMethod renamed to Request.Method
Request.Url changed to Request.GetDisplayUrl() (extension method)
Request.UserHostAddress changed to Request.HttpContext.Connection.RemoteIpAddress
Request.UserLanguages changed to Request.Headers[HeaderNames.AcceptLanguage]
Request.UrlReferrer changed to Request.Headers[HeaderNames.Referer]
Response.Cookies.Set() changed to Response.Cookies.Append()
FormsAuthentication.SetAuthCookie() changed to HttpContext.SignInAsync()
FormsAuthentication.SignOut() changed to HttpContext.SignOutAsync()
AjaxOnlyAttribute removed, consider your own implementation of this attribute
HttpException removed, consider to change your code to something else
OutputCacheAttribute removed, consider to change your code to something else
ChildActionOnlyAttribute removed, consider switching to View Components


I spent roughly three weeks to migrate the whole project to .NET Core and ASP.NET Core. Sometimes not everything was obvious. After spending this time, I can say that the source code became much cleaner, unit-testable, and maintainable. The overall performance of the application has increased as well. Yes, the juice was worth the squeeze.

Hopefully you will find something helpful for your migration story. Goodluck!