This article is part of the .NET Core Series. Go have a look at the other articles of this series, and run through the previous topics if not done already! This has lot of overlap & dependency on the previous article.
Here I’ll talk about my experience of converting an existing ASP.NET MVC application (.NET Framework) to the new ASP.NET Core 2.0 application. It is rather a port or migration than an upgrade, as it involved bunch of code changes and restructuring. Some of the concepts has changed dramatically, and some existing functionalities are not available anymore.
Before going into the actual porting process, let us briefly discuss about the practical changes and the real changes we are going to make. Here, when we say ASP.NET Core or .NET Core, we refer to the .NET Core 2.0 framework.
[1] MVC Application with ASP.NET Core
Web Application
Some of the distinctive features of new ASP.NET Core
web projects
- Target framework:
.NET Core 2.0
- Output type:
Console Application
- Not bound to heavy
System.Web
dll - Builds as a console application. Not tied to
IIS
and can be integrated with most web servers likeApache
,Nginx
,Docker
etc. - And all general notions of
.NET Core
- cross platform, open source, better optimized code written from scratch, lightweight & portable, NuGet based framework
[2] The ASP.NET Core MVC application - What remains same
- The core ideas and concepts of MVC as an architecture (obviously)
- The project structure - the Controllers & Views folder
- Model binding, model validation, selection of View etc.
- Layout & ViewStart
- MVC specific state management - ViewModel, ViewData, ViewBag, TempData, Session
- Razor based views - there are some syntax changes & enhances though
- Model
data-annotations
- The basic controller action still looks the same (can be
async
as well, see later)
public IActionResult Index()
{
return View();
}
[3] General changes for MVC project (compared to ASP.NET 4 or older)
- The project builds as a
Console
project - It has a
Program
class withMain()
method - this is called just once when the app starts for the first time - The
Startup
class. It is called fromMain
and has the initial configuration code for the application (which used be mostly inApplication_Start()
method ofGlobal.asax
) are inStartup
class. The main methods are- ConfigureServices() - optional, inject & configure services, manages lifetime
- Configure() - required, configure the
http pipeline
by adding necessary MiddleWare. A middleware basically works on theHttpContext
and then invokes next middleware in the pipeline or sends back response directly - The
Startup
can accept constructor dependencies defined by the host, likeIHostingEnvironment
,IConfiguration
etc.
- With the
ConfigureServices()
method,Dependency Injection
is built in - The
wwwroot
folder. All static files are kept here, files outside this folder are not be served directly. The name of the folder can be changed though - As the app runs with an internal web server (Kestrel) by default, it needs a full powered web server (like IIS, Nginx, Apache etc.) as a reverse proxy for production hosting
- Support for popular front-end or client-side development tools like
Gulp
,Grunt
,Bower
etc. Support is available both in VS and dotnet cli - Bundling & minification needs additional tools or NuGet
- There is no
ApiController
forWeb API
. Web API is built using the same infrastructure like MVC - There is no
[FromUri]
attribute. Use[FromQuery]
or attribute routing instead. - The
ActionFilter
s are still there, but has changed form. Following needs to be replacedSystem.Web.Http.Filters
withMicrosoft.AspNetCore.Mvc.Filters
HttpActionContext
withActionExecutingContext
forOnActionExecuting()
etc.
Razor
views haveTag Helper
, comparable to what used to beHTML Helper
.- If there is action method to support file upload, the old
HttpPostedFileBase
has to be replaced withIFormFile
and associated codes will need change too - Anti forgery is automatic and more customizable
- MVC routes are defined in
Configure()
method ofStartup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
- Controller actions are preferably
async
where there is a slow/network work
public async Task<IActionResult> Details(int? id)
{
if (id == null)
return NotFound();
var movie = await _context.Movie
.SingleOrDefaultAsync(m => m.ID == id);
if (movie == null)
return NotFound();
return View(movie);
}
- For all stuffs
MVC
like[HttpPost]
,[FromBody]
etc. base namespace changed fromSystem.Web.Mvc
toMicrosoft.AspNetCore.Mvc
- In controller methods,
HtmlEncoder.Default.Encode
can protect against malicious user inputs e.g.JavaScript
code
// Requires using System.Text.Encodings.Web;
public string Welcome(string name)
{
return HtmlEncoder.Default.Encode($"Hello {name}!");
}
- And many other changes & additions
[4] Configuration/Setting files in ASP.NET Core
When a new ASP.NET Core (MVC) Web Application is created, the following Configuration/Setting files are created
ProjectName.csproj
- the project settings, mainly target Framework(s), and NuGet packagesappsettings.json
- similar toapp.config
, runtime configuration like log profile, connection strings etc. This can be environment specific.bundleconfig.json
- static files (css, js etc.) bundling and minifications settingsProperties
/launchSettings.json
- app launch and debug profiles, IIS settings, start mode (IISExpress, Kestrel etc.), Environment (Development, Production etc.) etc.
Now the csproj
files are pretty clean and simple. See one sample file below, it’s quite self explanatory.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.1" />
</ItemGroup>
</Project>
[5] Configuration
- There’s no
web.config
(unless app is run with IIS). Configuration can be read from a bunch of sources, including- Files (JSON, XML, INI)
- Environment variables etc.
- In-memory local class object
- Command line arguments
$ dotnet run key1=value1
- Read more about Configuration
-
Also look at Options pattern in ASP.NET Core
- General convention is to keep settings in
- appsettings.json, or specific to environment like
- appsettings.{environment}.json , read about [6] Environments later
- The environment is typically set to
Development
,Staging
, orProduction
- Read more about Environments
- Configuration is read through injected
IConfiguration
instance. Some common methods are (assume IConfiguration instance is Configuration)- Configuration [“Profile:MachineNames:0:Domain”] configuration indexer
- Configuration.GetSection (“App:MainWindow”) method
- Configuration.GetValue
("App:MainWindow:Left", 80) extension
- One can bind a whole settings to class hierarchy/structure as
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var config = builder.Build();
//AppSeetings is a user defined class that matches appsettings App section
var appConfig = new AppSettings();
config.GetSection("App").Bind(appConfig);
//Or alternatively
var appConfig = config.GetSection("App").Get<AppSettings>();
- Sample
appsettings.json
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"MovieContext": "myCoolConnectionStringHere"
}
}
- Understand that Configuration.GetConnectionString (“ConnStrName”) methods used to get connection string, is nothing but shorthand for GetSection(“ConnectionStrings”) [“ConnStrName”]
See the practical configuration & DI article to see real life code demo on how configuration settings are used in .NET Core.
[6] Environments
ASP.NET Core configuration is environment-aware. That means configuration, settings and code can be written specific to specific application deployment environment. By default it understands 3 environments - Development
, Staging
& Production
(names are NOT case-sensitive in Windows
& Mac OS
, but IS case-sensitive in Linux
), but environment can be set to anything, e.g. ‘Pre-Staging’.
To set ASPNETCORE_ENVIRONMENT
variable
- If nothing is set, by default it’s taken as
Production
- Can be set through
dotnet cli
commands (commands vary per platform) per run of the application
set ASPNETCORE_ENVIRONMENT=Development
dotnet run
# or
set ASPNETCORE_ENVIRONMENT=Production && dotnet run
# or, to ignore launchsettings.json profiles
dotnet run --no-launch-profile
# or, to run with specific profile in launchSettings.json
dotnet run --launch-profile "Development"
- Machine wide value can be set in Windows environment variables in
Control Panel > System > Advanced system settings > Environment variables
- Local application settings can be set in
Properties
\launchSettings.json
. This takes precedence over system values. See samplelaunchSettings.json
below. Remember that- commandName Can be :
IIS
,IISExpress
orProject
(Kestrel) - ASPNETCORE_ENVIRONMENT can be
Development
,Staging
,Production
or anything - When app is run with
$ dotnet run
, it takes first profile withcommandName
:Project
- These different profiles are available as VS debuf profile under VS debug menu
- Profile & environment can be updated from
Project
>properties
>Debug
- commandName Can be :
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:60237/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"MvcMovie": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:60238/"
}
}
}
[7] Environment specific code
Now settings can be environment specific. In all cases, when app is running with
- Startup{EnvironmentName} e.g. StartupStaging class
- public void ConfigureDevelopmentServices (IServiceCollection services)
- public void ConfigurePreStaging (IApplicationBuilder app, IHostingEnvironment env)
- appSetting.Production.json
Also, code can be written to specific environment, both in cs
and views
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Dev_2"))
{
app.UseExceptionHandler("/Error");
}
@inject Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnv
<p> ASPNETCORE_ENVIRONMENT = @hostingEnv.EnvironmentName</p>
<environment exclude="Development">
<p><environment exclude="Development"></p>
</environment>
<environment include="Staging,Production,Staging_2">
<p>
<environment include="Staging,Production,Staging_2">
</p>
</environment>
[8] Areas
- Areas are just folders to keep bunch of related controllers, views, models together
- Individual areas do not have
MyAreaRegistration
class - For all of the areas to work, add following in
Startup.Configure()
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
); //if 1st part of uri matches existing area, it'll find controller there
[9] Filters
Filters or ActionFilter
s are pieces of code that can be wired into any controller actions so that they run automatically before/after the actual action code runs.
Since MVC
and Web API
are unified now, there is NO 2 flavours of filters like
System.Web.Mvc
System.Web.Http.Filters
They are now replaced by a single type of filter
Microsoft.AspNetCore.Mvc.Filters
There are different types of filters for different purposes, and they execute as an ordered pipeline
All the filters have two interface
for the sync
and async
variants. For, example, the ActionFilterAttribute
class implements both IActionFilter
and IAsyncActionFilter
. But, while implementation, implement only either of the sync or async type. The runtime will check for the async
versions first, if not found it’ll execute the sync
version.
Filters can be added to
- Action methods
- Controller classes
- Globally, in
AddMvc()
method inStartup
- Handling dependency injection in Filters can be tricky, you’ll have to use
TypeFilter
orServiceFilter
. See the practical configuration & DI article to see real life code demo on how it is done.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add(typeof(SomeActionFilter)); // by type
options.Filters.Add(new AnotherActionFilter()); // an instance
});
}
The order of execution is (unless overriden by implemention IOrderedFilter
)
Global OnExecuting
Controller OnExecuting
Action OnExecuting
Action OnExecuted
Controller OnExecuted
Global OnExecuted
Any Filter can short-circuit the rest of filter-pipeline by setting a value to context.Result
, or throwing Exception
.
Also,
- Learn about authorization in ASP.NET Core
- Use logging for your ASP.NET Core application
- Know about standard error handling
[10] Steps to port ASP.NET MVC app to ASP.NET Core 2.0 MVC
Before this, please read Porting existing .NET Framework libraries to .NET Core to understand the general porting process and other considerations.
- The
*.csproj
has changed a lot. So, better create a.NET Core 2.0
web project and select theMVC
template. Then add the code to it. - Since the project file does not list the reference
C#
files, just copying them over to the new project folder automatically includes them in the project. Copy all controllers, views, view models, helpers, filters etc. - Now try to build it. Obviously it’ll fail as it does not have the other project & NuGet references. Add them.
- Add the required project references. Make sure they already are .NET Core projects, else the port will not work.
- Add the required NuGet packages. Make sure the NuGet package targets
.NET Core
/.NET Standard
. Sometimes you might have to add multiple packages what used to be part of a single package.- Else look for alternative packages which can do the same job.
- Many 3rd party packages have unofficial .NET Core port, which works fine.
- Sometimes you need to select the “include pre-release” options in NuGet package manager to get latest version that works. Make sure the pre-release is stable.
- If none of this works, you might be stuck !
- Because of the NuGet changes, some of the
namespace
would have changed. Find out the new namespace and do a “Replace All” in your project. ReplaceSystem.Web.Mvc
withMicrosoft.AspNetCore.Mvc
. - Some of the APIs (the interface, class, method, parameters etc.) might have changed as well. Make necessary changes in the code so that it complies with the new APIs.
web.config
does not work as they used to. Ideally move all the required settings toappsettings.json
file or other configuration sources. Make necessary code changes to use settings from the new source.- Any code that needs to run at application startup (those which were called from
Application_Start
method inGlobal.asax
), move them insideStartup
class. - There is no
HttpContext.Current
in ASP.NET Core. Idea is to not litter whole code with random access to current HttpContext. Controllers have anHttpContext
property that can be used for this purpose, middleware would get HttpContext injected inInvoke()
method. For anything else,IHttpContextAccessor
can be injected, which has access to current threadHttpContext
with a property of same name. Register it in Startup, AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); - No
AssemblyInfo
class is created by default. If required, add it (e.g. for assembly level settings like “internals visible to”). Like [assembly: InternalsVisibleTo(“TestProj”)] - Remove all {AreaName}AreaRegistration.cs from all areas. Add area route as mentioned in section [8]. Remove
web.config
from views folders. - Make all necessary changes in
ActionFilter
, if any, as mentioned in section [9]. - Put all static files like
js
,css
,html
, images etc. inside thewwwroot
folder. they can be kept somewhere else and be moved there later with bundling or built task, but keeping them there will follow the conventions better. Fix all the hard coded paths accordingly. - Update
bundleconfig.json
with all file details to be bundled and minified. Run the bundling task to generate the bundled files. Make sure the created files stay inwwwroot
folder. - Remove any use of
JsonRequestBehavior.AllowGet
. For uploading files, useIFormFile
in place ofHttpPostedFileBase
and change corresponding code. - Move any custom routes to
app.UseMvc()
inStartup
, or use attribute route in specific controller classes. - BUILD IT. Fix any compilation errors.
- Run it locally with Kestrel or IISExpress. Run all necessary tests.
- If all goes fine, configure it to run with a standard web server e.g.
This article covered the process of porting existing ASP.NET MVC
web applications to .NET Core 2.0
MVC application. Continue to Porting Web API services to ASP.NET Core 2.0.