Migrating an ASP.NET MVC 5 App to ASP.NET 5 (RC1)
ASP.NET 5 is an amazing reinvention of ASP.NET. When targeted against the .NET Core CLR, ASP.NET is cloud-optimized and cross-platform. Many have already written about the virtues of the new platform. But in this article, I will show how to take a medium-small demo app written using Visual Studio 2013, ASP.NET 4.5, MVC 5, and Entity Framework 6 and turn it into a working ASP.NET 5 app employing Visual Studio 2015, MVC 6 and Entity Framework 7. And the new app will happily run on either the .NET 4.6 CLR or the .NET Core CLR. Let's get started. [This post was updaed originally published on October 28, 2015 for Beta 8; it was updated for RC1 on January 9, 2016]
The Random Acts of Kindness home page.The Random Acts Web Application
I originally created the RandomActs web application for a talk on unit testing I gave a few times in 2013 and 2014. Random Acts is a fictitious site for tracking random acts of kindness. It’s sort of like EventBrite for community “do good” projects. You use it to create Acts (events), Actors (volunteers), and match actors to acts. The data is stored in a SQL Server database of three tables, RandomActs, RandomActors, and RandomActActors.
RandomActs goes a little further than some bare bones demo projects in that it uses repository classes and the controllers are hooked up to the repository classes using dependency injection. Also, it makes uses of Entity Framework’s lazy loading feature to count the number of actors in an act and the number of acts that a given actor is associated with. It also allows supports the concept of a waiting list when the number of actors for an act exceeds the maximum number of actors allowed.
Though the steps are pretty specific to my example app, I believe they would be similar to the steps you will would need to follow in order to migrate any application. Obviously, the more complex your project, the more work it will require, especially if you employ multiple projects and third-party libraries. Regardless, I hope you will find this post useful in at least getting you pointed in the right direction.
Versions and Repos
This post was written on Beta 8 updated for RC1 version of ASP.NET 5 and Entity Framework. This version is pretty stable but there’s no guarantee that some of what is discussed in this article will not change for RC2 or the final release. Your beta testing mileage may vary.
The ASP.NET 4.x version of the project can be cloned here: https://github.com/plitwin/RandomActs.git
The ASP.NET 5 (RC1) version can be cloned here: https://github.com/plitwin/RandomActs5.git
Before We Get Started
The file structure of an ASP.NET 5 project is a radical departure from the file structure used by prior versions of ASP.NET MVC projects. Some of the differences:
- The web project files live under src folder.
- All static files have moved under a subfolder of src named wwwroot.
- The web.config is gone and replaced by a series of json files, with the major one named project.json.
- Global.asax is also gone as is the App_Start folder. Routing has been moved to a new file named startup.cs.
- startup.cs also handles dependency injection, which is baked in by default now.
What all this means is that you can’t just take an existing ASP.NET MVC project and open it up in Visual Studio 2015 and click the Migrate button. It doesn’t exist. Not at this point; not sure if it ever will. Nor can you create an empty ASP.NET 5 app and drag and drop your existing project’s files into the project and expect success. Due to the nature of the changes, a more subtle approach is needed.
Step 1: Creating a New Project
From Visual Studio 2015, select File|New Project. Select the ASP.NET Web Application, name your solution, and click OK. At the next dialog, select the ASP.NET 5 Application template. For simplicity, I changed the default authentication from Individual User accounts to No authentication but you can leave it on if you’d like.
At this point, try running the project to make sure it works. If not, something went wrong and I would get that fixed before moving forward.
Step 2: Copying Existing Project Files
Time to copy in some files. Due to the aforementioned issue with the two ASP.NET versions having wildly different file structures and functionality of some of the files, I took the following approach to copying files:
- Create a Models folder in the new project and copy each of the Model files from the ASP.NET 4 project into the new project’s Model folder.
- Copy all the Controller files from the ASP.NET 4 project to same folder in the ASP.NET 5 project.
- Copy the following subfolders under Views to the new project’s Views folder:
- Act
- ActActor
- Actor
- Do not copy any other views.
Step 3: Copy Navigation Markup
Since ASP.NET 5 changes the _Layout.chshtml file significantly (mainly in how client-side resources are pulled in), you will need to copy just select pieces of this file into the new project’s _Layout.cshtml (found under Shared views):
- Fixup the title so it correctly identifies the site. For the footer, copy everything between <footer></footer> over the new project’s footer.
- Locate this tag helper in the new project that creates the link to the home page:
<a asp-controller="Home" asp-action="Index" class="navbar-brand">New Project</a>
Change “Home” to “Act”, and whatever you named your “New Project” to “Random Acts of Kindness”. - Right under this anchor tag helper you will find the navigation unordered list with three list items each containing an anchor tag helper. As with the home page link, update the three elements so they point to the correct controller and methods. Add a fourth element. The final list should look like this:
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-controller="Act" asp-action="Index">Events</a></li>
<li><a asp-controller="Actor" asp-action="Index">Volunteers</a></li>
<li><a asp-controller="Act" asp-action="About">About</a></li>
<li><a asp-controller="Act" asp-action="Contact">Contact</a></li>
</ul>
</div>
Step 4: Add Entity Framework 7
- Open the project.json file and add the following two pre-release packages (this should cause the nuget packages for these two dependencies to be downloaded to the project) to the end of the Dependencies section:
"EntityFramework.Commands": "7.0.0-rc1-final",
"EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final"
- Add the following command to the commands section of project.json:
"ef": "EntityFramework.Commands"
Step 5: Add the Connection String
Add the following to the appsettings.json file:
"ConnectionStrings": {
"RandomActsDB" : "Server=.;Database=RandomActs5DB;Trusted_Connection=True;MultipleActiveResultSets=True;"
}
This assumes you have a local instance of SQL Server. Change the connection string accordingly if you are using LocalDb, or some other Sql Server variant.
Step 6: Register Entity Framework with Dependency Injection
- Add the following using statements to the top of the startup.cs:
using Microsoft.Data.Entity;
using RandomActs.Models;
- Add the following code to the Configure Services method.
var cnx = Configuration.Get<string>("ConnectionStrings:RandomActsDB");
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<RAOKContext>(options => options.UseSqlServer(cnx));
Step 7: Global Search and Replace
Use search and replace to make the following replacements across the entire project:
- Replace "System.Data.Entity" with "Microsoft.Data.Entity"
- Replace "System.Web.Mvc" with "Microsoft.AspNet.Mvc"
- Replace "using System.Web;" with an empty string (effectively deleting all these using statements)
- Replace "HttpStatusCode.BadRequest" with "(int) HttpStatusCode.BadRequest"
Step 8: Fixup Bind syntax
The syntax of the Bind attribute has been simplified. Instead of using [Bind(Include="item1, item2, item3")], you now use [Bind("item1","item2","item3")] . For example, change the Bind attributes from this syntax:
public ActionResult Edit([Bind(Include="RandomActId,Title")] RandomAct randomact)
to this:
public ActionResult Edit([Bind("RandomActId","Title")] RandomAct randomact)
TIP: You should be able to quickly find all the places to fix this code by compiling the project and looking at the Error List window.
Step 9: SelectList fix
For any controllers making use of the SelectList method, add the following using statement:
using Microsoft.AspNet.Mvc.Rendering;
Again, use the Error List window to quickly locate the problem methods.
Step 10: Fix up Entity Framework Find Methods
In the repository classes, replace any Entity Framework Find methods with SingleOrDefault methods containing a lambda expression limiting the rows to where the primary key value equals the passed in value.
For example, change this:
context.RandomActors.Find(id);
to this:
context.SingleOrDefault(x => x.RandomActorId == id);
Step11: Fixup Route
Change the route section of the startup.cs to use the Act controller as the default:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Act}/{action=Index}/{id?}");
}
Step 12: Setup Dependency Injection for Repository Classes
- Back to the startup.cs file. Add the following statements to the ConfigureServices method to inject each repository class into the appropriate controller class:
services.AddScoped<IRandomActRepository, RandomActRepository>();
services.AddScoped<IRandomActorRepository, RandomActorRepository>();
services.AddScoped<IRandomActActorRepository, RandomActActorRepository>();
- Each controller class has two constructors, the first one of which is now no longer needed because of the above dependency injection. Delete the first constructor (the one with the "this" reference) in each controller class:
public ActController() : this(new RandomActRepository())
{
}
Step 13: Change DbContext References in Repository Classes to Support Dependency Injection
- Remove the following from the constructor of the RAOKContext class in RAOKContext.cs (alternately, you can remove the entire constructor)
: base("RandomActsDB")
- The project should now compile without error. If not, review the steps. If you run the project, however, you will get a runtime error complaining about the lack of a database connection. Thiis error took some head scratching until I had an "ah ha" moment and realized the exception was occurring because each of the repository classes needed to be modified to receive the injected DbContext.
- For each repository class, delete the statement that instantiates the DbContext:
RAOKContext context = new RAOKContext();
- Replace the above code at the top of each of the three repository classes with the following constructor (and private variable):
public RAOKContext context { get; private set; }
public RandomActActorRepository(RAOKContext raokcontext)
{
context = raokcontext;
}
The above example is for the RandomActActorRepository. Be sure to use the appropriate constructor name for the other two repository classes.
Step 14: Creating the Database
I didn't originally include the steps to create the database in SQL Server because I was migrating an existing database. Alas, you probably don't already have the database created. If not, you need to do a few extra command line steps in the current version of EF. And you can only do this once your project compiles, so that's why it's way down here. Open a VS command prompt (or any command prompt), navigate the the project folder (the folder under scr) and enter the following two dnx ef commands into the command prompt:
dnx ef migrations add CreateRandomActsDB
dnx ef database update
This will scaffold the migrations to create the database using ef migrations and then submit those changes to SQL Server. Note: ef will use the connection string you added earlier in Step 5 so if it's wrong, you will need to fix it there first.
Step 15: Remove jqueryval Script References
Several of the pages in the project make use a script bundle that is no longer needed and that causes runtime errors. Remove the following code from the very bottom of the Edit and Create views in the Act views folder and the Edit view of ActActor views folder. Here is the code to delete:
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Step 16: Fix up Lazy Loading Assumptions
If you compile and run the app at this point, you will encounter the beloved ArgumentNullException: Value cannot be null. This one took me a while to figure out until I remembered reading about the fact that lazy loading is not implemented in Entity Framework 7 RC1. If you've ever coded against prior version of EF, you are used to utilizing the magic of lazy loading (even if you didn't call it that) to navigate entity relationships and implicitly load the related data when needed. Well, that doesn't work in the current beta version of EF (but it might work by the time it ships).
The solution is to explicitly enable eager loading whenever related entities will be needed for a given entity. The RandomActs app took advantage of lazy loading for the volunteer dropdown in the ActActorContoller class and for the counts that are displayed on the views of the ActController and the ActorController classes.
To fix:
- Replace the following code from the Index method of the ActController which is expecting lazy loading:
return View(actRepository.All);
with this method which employs eager loading:
return View(actRepository.AllIncluding(x => x.Actors));
- Similarly, replace the code from the Index method of the ActorController class that look like this:
return View(actorRepository.All);
with this:
return View(actorRepository.AllIncluding(x => x.Acts));
- The AttendeeList method of the ActActorController class also has lazy loading issues. But to fix the issues requires fixes in two different classes. First, modify the Find method in the RandomActRepository class (note that we already edited this method once in Step 10) from this:
return context.RandomActs.SingleOrDefault(x => x.RandomActId == id);
to this:
return this.AllIncluding(x => x.Actors).SingleOrDefault(x => x.RandomActId == id);
- You also need to open the RandomActActorRepository class and modify the code in the FilterByActId method from
return context.RandomActActors.Where(x => x.RandomActId == actId);
to this
return context.RandomActActors.Where(x => x.RandomActId == actId).Include(x => x.Actor).Include(x => x.Act);
- Finally, the ActActorController class' Edit method also has a lazy loading expectation that can be fixed by changing this code in the RandomActActorRepository class' Find method (also edited under Step 10 previously) from this:
return context.RandomActActors.SingleOrDefault(x => x.RandomActActorId == id);
to this:
return context.RandomActActors.Include(x => x.Actor).Include(x => x.Act).SingleOrDefault(x => x.RandomActActorId == id);
Optional Steps
The migrated RandomActs project should now compile and run without error. There are, however, a couple of optional changes you may wish to make to be more consistent with ASP.NET 5:
- Search and replace all "ActionResult" controller method return types for "IActionResult".
- Replace all Html helper methods with the Tag Helper syntax. Thus, for example,
@Html.TextBoxFor(model => model.Title, new { style = "width: 400px" })
becomes:
<input asp-for="Title" style="width: 400px" />
and:
@using (Html.BeginForm())
{
}
becomes:
<form asp-controller="Act" asp-action="Edit" method="post">
</form>