.NET Core RC2 is a major update from the November RC1 release and since announced all those that developed apps using the RC1 version are now required to upgrade them. The new version of .NET Core includes new APIs, performance and reliability improvements and a new set of tools as well. This means that not migrating your RC1 apps to it is not even an option. On the very first day of this year I released the Cross-platform SPAs with ASP.NET Core 1.0, Angular 2 & TypeScript post where we have seen how to get started developing Single Page Applications using ASP.NET 5, Angular 2 and Typescript. The post started with ASP.NET 5 beta and Angular 2 beta versions as well, but I made a promise that I will be upgrading the app as soon as new releases are out. Angular 2 was upgraded to RC.1 and now it’s time for the big change we have all waiting for. Upgrade from ASP.NET 5 RC1 to ASP.NET Core.
What is this post all about
This post will describe the changes needed in order to upgrade the PhotoGallery SPA application we built together, from ASP.NET 5 RC.1 to ASP.NET Core. You do know the PhotoGallery app or not, it really doesn’t matter. In case you have your own ASP.NET 5 application and you are interesting to migrate it, then you are on the right place. The interesting part is that the PhotoGallery app had incorporated many important features such as Entity Framework Core 7 with migrations and MVC services so you will have the chance to see not only the changes required to get the upgrade right but also some problems I encountered during the process. Before starting let me inform you that I have moved the ASP.NET 5 version of the project to its own Github branch named RC_1 so it is always available as a reference. The master branch will always contain the latest version of the app. You can view the RC_1 branch here.
Starting migration…
The first thing you have to do is remove all previous versions of .NET Core from your system which obviously is different for different operating systems. On Windows you can do this through the control panel using Add/Remove programs. In my case I had two versions installed.
Believe it or not this is where I got the first issue and un-installation failed. I got a setup blocked message for some reason and also asked for a specific .exe file in order for the process to continue. It turned out that the web installer file required was this file so in case you get the same exception download it and select it if asked. At the end of this step you shouldn’t have any version of ASP.NET 5 in the Add/Remove programs panel.
Install .NET Core SDK
Depending on the OS you use, follow the exact instructions described here. I had VS 2015 already installed so I continued the process with the Visual Studio official MSI Installer..
… and the NuGet Manager extension for Visual Studio..
When all these finished make sure that the new .NET Core CLI has been successfully installed by typing the following command in a console.
dotnet --version
In a nutchel, .NET Core CLI replaces the old DNX tooling. This means no more DNX, dnu or dnvm commands, only dotnet. Find more about their differences here.
By the way in case you want to remove DNVM you have two options. Either run dnvm list to get all installed versions and then run dnvm uninstall version_to_delete or simply remove the runtime folders from the user’s profile folder.
dnvm list dnvm uninstall version_to_delete
After doing this, if you open a new terminal dnvm will not be recognized by the system. For deleting DNU and DNX delete the %USERPROFILE%\.dnx folder and any reference exists in the PATH environment variable.
Project Configuration
It’s time to open the PhotoGallery ASP.NET 5 application and convert it to ASP.NET Core one. Open the solution (or your own ASP.NET 5 project) and make sure you have the Github RC_1 branch version. I must say that at this point and ASP.NET 5 uninstalled the project still worked as charmed. The first thing you need to change is the SDK version that the application is going to use. This is being set in the global.json folder under the Solution items folder. Change it as follow:
{ "projects": [ "src", "test" ], "sdk": { "version": "1.0.0-preview1-002702" } }
Notice that this is the exact version that the previous command printed. We continue with the project.json. Before showing you the entire file let’s point some important changes. The compilationOptions changes to buildOptions as follow:
"buildOptions": { "emitEntryPoint": true, "preserveCompilationContext": true }
Target frameworks declaration changes as follow:
"frameworks": { "dnx451": { }, "dnxcore50": { "dependencies": { "System.Security.Cryptography.Algorithms": "4.0.0-beta-23516" } } }
"frameworks": { "netcoreapp1.0": { "imports": [ "dotnet5.6", "dnxcore50", "portable-net45+win8" ] } }
The old publish and excludePublish options have been replaced with the publishOptions as follow:
"exclude": [ "wwwroot", "node_modules" ], "publishExclude": [ "**.user", "**.vspscc" ]
"publishOptions": { "include": [ "wwwroot", "Views", "appsettings.json", "web.config" ], "exclude": [ "node_modules" ] }
Any 1.0.0-rc1-final depedency should change to 1.0.0-rc2-final and any Microsoft.AspNet.* dependency to Microsoft.AspNetCore.*. For example..
"Microsoft.AspNet.Authentication.Cookies": "1.0.0-rc1-final"
.. changed to
"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-rc2-final"
The commands object we knew has been changed to a corresponding tools object. Here is the entire ASP.NET Core version of the project.json file.
{ "webroot": "wwwroot", "userSecretsId": "PhotoGallery", "version": "2.0.0-*", "buildOptions": { "emitEntryPoint": true, "preserveCompilationContext": true }, "dependencies": { "AutoMapper.Data": "1.0.0-beta1", "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-rc2-final", "Microsoft.AspNetCore.Diagnostics": "1.0.0-rc2-final", "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0-rc2-final", "Microsoft.AspNetCore.Identity": "1.0.0-rc2-final", "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final", "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0-rc2-final", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final", "Microsoft.AspNetCore.StaticFiles": "1.0.0-rc2-final", "Microsoft.EntityFrameworkCore": "1.0.0-rc2-final", "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0-rc2-final", "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview1-final", "type": "build" }, "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc2-final", "Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final", "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-rc2-final", "Microsoft.Extensions.FileProviders.Physical": "1.0.0-rc2-final", "Microsoft.NETCore.App": { "version": "1.0.0-rc2-3002702", "type": "platform" } }, "tools": { "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview1-final", "imports": "portable-net45+win8+dnxcore50" }, "Microsoft.AspNetCore.Server.IISIntegration.Tools": { "version": "1.0.0-preview1-final", "imports": "portable-net45+win8+dnxcore50" }, "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview1-final", "imports": [ "portable-net45+win8+dnxcore50", "portable-net45+win8" ] }, "Microsoft.Extensions.SecretManager.Tools": { "version": "1.0.0-preview1-final", "imports": "portable-net45+win8+dnxcore50" }, "Microsoft.VisualStudio.Web.CodeGeneration.Tools": { "version": "1.0.0-preview1-final", "imports": [ "portable-net45+win8+dnxcore50", "portable-net45+win8" ] } }, "frameworks": { "netcoreapp1.0": { "imports": [ "dotnet5.6", "dnxcore50", "portable-net45+win8" ] } }, "runtimeOptions": { "gcServer": true, "gcConcurrent": true }, "publishOptions": { "include": [ "wwwroot", "Views", "appsettings.json", "web.config" ], "exclude": [ "node_modules" ] }, "scripts": { "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] } }
Feel free to compare it with the ASP.NET 5 version. I have highlighted some important dependencies cause you certainly cannot ignore. For example you need to declare the Microsoft.EntityFrameworkCore.Tools if you want to work with EF migrations. At this point I noticed that Visual Studio was complaining that the NPM packages weren’t successfully installed. More over it seemed that it was trying to download extra packages not defined in the package.json as well.
What I did to resolve this is make Visual Studio use my own Node.js version. Right clink on the npm folder and select Configure External Tools.
Add the path to your Node.js installation folder and make sure to set it to the top. VS will use this from now on.
Code refactoring
After all those settings I believe the solution had at least 200 compilation errors so my reaction was like..
The first thing I did is fix all the namespaces. If you remember we renamed all Microsoft.AspNet.* dependencies to Microsoft.AspNetCore.* so you have to replace any old reference with the new one. Another important naming change is the one related to Entity Framework. The core dependency in the project.json is the “Microsoft.EntityFrameworkCore”: “1.0.0-rc2-final” which means there is no Microsoft.Data.Entity any more. Let’s compare the namespaces in the PhotoGalleryContext class which happens to inherit DbContext:
using Microsoft.EntityFrameworkCore; using PhotoGallery.Entities; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace PhotoGallery.Infrastructure { public class PhotoGalleryContext : DbContext {
Compare it with the old version. You can find more info about upgrading to Entity Framework RC2 here.
Here is an example of namespace changes all MVC Controller classes needed:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using PhotoGallery.Entities; using PhotoGallery.ViewModels; using AutoMapper; using PhotoGallery.Infrastructure.Repositories; using PhotoGallery.Infrastructure.Core; using Microsoft.AspNetCore.Authorization; namespace PhotoGallery.Controllers { [Route("api/[controller]")] public class AlbumsController : Controller { // code omitted
One of the key changes in ASP.NET Core is how the application fires. You need to define a Main method in the same way you would as if it was a console application. Why? Because believe it or not ASP.NET Core applications are just console applications. This means that you need to define an entry point for your application. You have two choices: Either create a new Program.cs file and define it over there or instead use the existing Main method in the Startup.cs file as follow:
// Entry point for the application. public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .Build(); host.Run(); }
Here is the updated Startup class. The highlighted lines are most important changes.
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.PlatformAbstractions; using Microsoft.Extensions.Configuration; using PhotoGallery.Infrastructure; using Microsoft.EntityFrameworkCore; using PhotoGallery.Infrastructure.Repositories; using PhotoGallery.Infrastructure.Services; using PhotoGallery.Infrastructure.Mappings; using PhotoGallery.Infrastructure.Core; using System.Security.Claims; using Microsoft.AspNetCore.StaticFiles; using System.IO; using Microsoft.Extensions.FileProviders; namespace PhotoGallery { public class Startup { private static string _applicationPath = string.Empty; private static string _contentRootPath = string.Empty; public Startup(IHostingEnvironment env) { _applicationPath = env.WebRootPath; _contentRootPath = env.ContentRootPath; // Setup configuration sources. var builder = new ConfigurationBuilder() .SetBasePath(_contentRootPath) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); if (env.IsDevelopment()) { // This reads the configuration keys from the secret store. // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 builder.AddUserSecrets(); } builder.AddEnvironmentVariables(); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; set; } // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddDbContext<PhotoGalleryContext>(options => options.UseSqlServer(Configuration["Data:PhotoGalleryConnection:ConnectionString"])); // Repositories services.AddScoped<IPhotoRepository, PhotoRepository>(); services.AddScoped<IAlbumRepository, AlbumRepository>(); services.AddScoped<IUserRepository, UserRepository>(); services.AddScoped<IUserRoleRepository, UserRoleRepository>(); services.AddScoped<IRoleRepository, RoleRepository>(); services.AddScoped<ILoggingRepository, LoggingRepository>(); // Services services.AddScoped<IMembershipService, MembershipService>(); services.AddScoped<IEncryptionService, EncryptionService>(); services.AddAuthentication(); // Polices services.AddAuthorization(options => { // inline policies options.AddPolicy("AdminOnly", policy => { policy.RequireClaim(ClaimTypes.Role, "Admin"); }); }); // Add MVC services to the services container. services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // this will serve up wwwroot app.UseFileServer(); // this will serve up node_modules var provider = new PhysicalFileProvider( Path.Combine(_contentRootPath, "node_modules") ); var _fileServerOptions = new FileServerOptions(); _fileServerOptions.RequestPath = "/node_modules"; _fileServerOptions.StaticFileOptions.FileProvider = provider; _fileServerOptions.EnableDirectoryBrowsing = true; app.UseFileServer(_fileServerOptions); AutoMapperConfiguration.Configure(); app.UseCookieAuthentication(new CookieAuthenticationOptions { AutomaticAuthenticate = true, AutomaticChallenge = true }); // Custom authentication middleware //app.UseMiddleware<AuthMiddleware>(); // Add MVC to the request pipeline. app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); // Uncomment the following line to add a route for porting Web API 2 controllers. //routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}"); }); DbInitializer.Initialize(app.ApplicationServices, _applicationPath); } // Entry point for the application. public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .Build(); host.Run(); } } }
IApplicationEnvironment changed to IHostingEnvironment and also the way you add Entity Framework services to the application service provider. You may ask yourself what happens now that Entity Framework migrated to RC2? Do EF migrations work as used to? The answer is yes, there aren’t huge changes in the way you use EF migrations. I encountered though an issue while trying to add migrations so let me point it out. First of all make sure you have all the required dependencies and tools defined in order to use EF migrations. Then open the Package Manager Console and instead of running the old command dnx ef add migrations run the following:
dotnet ef migrations add initial
When I run the command I got the following error:
It turns out that if you run the command from the Powershell 5 you wont get the error. If you still want to run commands from the Package Manager Console as I did the only thing to do is navigate to the project’s root first using a cd file_to_root command and then run the command. Here’s what I did.
Then I run the database update command and the database was successfully created.
Launching
There are two more changes I did before firing the application on IIS. Firstly I changed the launchSettings.json file under Properties as follow:
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:9823/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "$safeprojectname$": { "commandName": "Project", "launchBrowser": true, "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
I have also modified the .xproj project file and replace the old DNX references with the new DotNet.
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> </PropertyGroup> <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> <PropertyGroup Label="Globals"> <ProjectGuid>33f89712-732c-4800-9051-3d89a2e5a1d9</ProjectGuid> <RootNamespace>PhotoGallery</RootNamespace> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> <TargetFrameworkVersion>v4.6</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup> <SchemaVersion>2.0</SchemaVersion> </PropertyGroup> <ItemGroup> <DnxInvisibleFolder Include="bower_components\" /> </ItemGroup> <Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" /> </Project>
Having done all these changes I was able to launch the app both from IIS and the console. In order to run the app from the console type the dotnet run command.
Conclusion
That’s it, we have finally finished!
Migrating an ASP.NET 5 application to ASP.NET Core is kind of tricky but certainly not impossible. Now you can also create a brand new ASP.NET Core Web application through Visual Studio 2015 by selecting the respective template.
As far as PhotoGallery SPA I used for this post, as I mentioned the master branch will always have the latest updates while the RC_1 keeps the ASP.NET 5 RC1 version. You can check the Upgrade from ASP.NET 5 to ASP.NET Core commit here. The full source code is available here with instructions to run the app in and outside of Visual Studio.
In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.
.NET Web Application Development by Chris S. | |||