May 2011

Volume 26 Number 05

Web Migration - Moving Your Web App from WebMatrix to ASP.NET MVC 3

By Brandon Satrom | May 2011

This past January, Microsoft introduced a new programming model for Web development with the Microsoft .NET Framework called ASP.NET Web Pages. Currently supported by default in WebMatrix (http://www.microsoft.com/web/webmatrix/next/), Web Pages is a page-centric programming model that behaves much like PHP, where each page contains its own business logic, data access and dynamic content for rendering HTML to the browser.

There are a variety of reasons to build a Web site in WebMatrix. But what if you know you want to move to Visual Studio at some point in the future? If ASP.NET MVC 3 is your end state, will you need to re-develop the site when the time for migration arises? If you’re afraid of boxing yourself in with WebMatrix and Web Pages, fear not.

Web Pages—as a part of the core ASP.NET framework—was built with flexibility in mind. And while there are no technical limitations to Web Pages that would ever force you to move to ASP.NET MVC, there may be times where it makes sense for your team, product or company to do so.

In this article, we’ll discuss some of the reasons why you might choose to migrate (as well as some of the reasons not to). We’ll also discuss strategies for moving your Web Pages site to ASP.NET MVC if you choose to do so. We’ll cover how to move from Pages to Views, how to handle business logic and helper code, and how to introduce Models in your application. Finally, we’ll discuss and show how to preserve existing site URLs through routing, and supporting permanent redirects when necessary.

When to Migrate?

Before we dive into reasons to migrate from Web Pages to ASP.NET MVC, let’s discuss a few reasons not to make such a move. For starters, you shouldn’t move your site from Web Pages to ASP.NET MVC because you’re afraid your application won’t scale. Because Web Pages is built on top of ASP.NET, it offers many of the same performance characteristics of applications written for ASP.NET MVC or Web Forms. Obviously, each application type has a slightly different execution model, but there’s nothing about ASP.NET MVC out of the box that makes it inherently more or less scalable than a Web Pages application. Scale and performance is just as much about the design decisions you make as you build your site, as it is about the underlying framework you choose.

It’s also not a good idea to move your site from Web Pages to ASP.NET MVC because it’s cool, sexy and you hear everyone is doing it, or just because you want to work with your site in Visual Studio—you can do that with Web Pages sites already. ASP.NET MVC is a choice in Web application architecture, not a magic wand that makes your site instantly better. Migration from Web Pages to ASP.NET MVC is a choice, not a necessity. And while Microsoft has done a fantastic job of making this migration possible and painless, it will still cost you time, resources and money, as any migration would. For these reasons, it’s important to make certain that you’re migrating to ASP.NET MVC for the right reasons.

One valid reason might be because unit testing is important to you and your team. Because the Web Pages model is page-centric, it’s not possible to use existing unit-testing tools with Web Pages sites. Web UI testing—using tools like WatiN or Selenium—is still possible, but code-level unit testing with tools like NUnit or MsTest is not. If your site has grown in complexity and unit testing is important to you, a migration to ASP.NET MVC makes sense.

If you prefer to unit test your code, chances are you also prefer some measure of separation of concerns in your applications. And while it’s possible to create clean, separated code using helpers and code files in Web Pages, the model doesn’t lend itself to this separation as naturally as ASP.NET MVC does. If separation of concerns is important to you, and you want an application architecture that encourages such separation, migration is a valid choice.

Beyond these two, other reasons for migration might come into play depending on the context of your site or organization. If you have a growing team and a site that’s increasing in complexity and requires richer business functionality, you’d be wise to migrate. Migration may also be necessary in order to make better use of a richer development ecosystem for things such as source control, load testing and so on.

Preparing for Migration

For this article, we’re going to take the Photo Gallery template site that ships with WebMatrix and migrate it to ASP.NET MVC. The core of the migration process is moving from Pages to Models, Controllers and Views, but we need to perform a bit of prep work before we can get there.

Because we’re detailing steps involved in a manual migration in this short article, we won’t be able to give every step equal attention. Our goal is to address the major steps, and at least mention the minor considerations. We’ve also chosen to omit items that aren’t related to the core of the overall conversion, such as data-access best practices, potential assembly structure, dependency injection and the like. These items are important, but many of them will come down to development culture and personal preference, and can all be addressed in your post-migration refactorings.

It’s also important to note that we’re not making use of the Open in Visual Studio feature in WebMatrix for this article, which will open your current site as a Web Site project in Visual Studio. You’re welcome to use this option, even if you choose not to migrate to ASP.NET MVC, but we prefer to use the Visual Studio Web Application project type for ASP.NET MVC, starting with an empty site and migrate things over manually.

As such, we’ll begin the migration by selecting File | New | Project and selecting an ASP.NET MVC 3 Web Application with the empty template, using Razor as the default view engine.

Once you have your target application set up, you’ll need to perform some initial migration work. Here’s a rundown of the initial steps:

  1. Add any packages you’re using in your Web Pages site into your ASP.NET MVC site via NuGet (nuget.org).
  2. Add references to System.Web.Helpers, WebMatrix.Data and WebMatrix.WebData. Set each to Copy Local = true in the properties pane.
  3. Move the contents of _AppStart.cshtml to the Application_Start method of Global.asax. While it’s possible to move and use _AppStart as is, we recommend centralizing its logic into Global.asax with existing ASP.NET MVC startup code.
  4. Add <roleManager enabled=true /> to the <system.web> section of your root web.config. The Photo Gallery application uses the new WebSecurity membership provider found in WebMatrix.WebData, so we’ll need that entry in our configuration for the site to function.
  5. Move any stylesheets, script files and images under the Content or Scripts folders in your application. Update any resource links in those files to their new paths.
  6. Modify the default Route in Global.asax to point to the Gallery controller and the Default action.
  7. Copy the SQL Compact Database found in the App_Data folder to the App_Data Folder of your site. If you’re using another database for your site, add that connection string to the Web.Config file in your application.

Moving from Pages to Views

Once you complete the initial setup of your ASP.NET MVC site, you’re ready to migrate the core of your exiting site: the Pages. In Web Pages, a Page (.[cs/vb]html) contains markup, business logic and any data access needed for that page. The main component of your work during a migration to ASP.NET MVC will be to break each page up and divide its content into Controller actions (business logic), data-access classes (data access) and Views (markup).

First, you need to migrate the Layout of your site. Similar to Master Pages in Web Forms and ASP.NET MVC, Layout pages are files that specify the layout structures of your site. Web Pages and ASP.NET MVC 3 (when used with the Razor view engine) both use the same Layout subsystem, so this portion of the migration should be easy. In the Photo Gallery site, the root _SiteLayout.cshtml file contains our site structure. Copy the contents, then navigate to your ASP.NET MVC site. Open the Layout file located at Views/Shared/_Layout.cshtml and paste in the contents of _SiteLayout.cshtml.

Once completed, you’ll need to make a few minor changes to _Layout.cshtml. First, change the link to your stylesheet to the new location in your ASP.NET MVC application (~/Content/Site.css instead of ~/Styles/Site.css). Second, you’ll need to change @Page.Title to @ViewBag.Title. Both are objects of type dynamic that can contain display or other data for pages in your site and, as you may have guessed, Page is used for Web Pages, while ViewBag is used for ASP.NET MVC.

The last thing you need to change in your _Layout.cshtml is something you should keep in mind for all of the pages you migrate to ASP.NET MVC. Notice that _Layout.cshtml uses @Href calls to insert URLs into the page. For any call that references static content (scripts, CSS and so on), these can stay unchanged. You will, however, want to change all of the @Href calls that point to pages on your site. While these will also work as is after your migration, they point to static URLs. In ASP.NET MVC, it’s considered a better practice to use ASP.NET Routing to create URLs for when Views are rendered. The result is cleaner, less-brittle links are tied to your Route Table definitions, rather than hardcoded on the site.

As such, you’ll want to change any links like the following:

<div id="banner">
  <p class="site-title">
    <a href="@Href("~/")">Photo Gallery</a>
  </p>
...
</div>

Instead, you’ll use @Url.RouteUrl or @Url.Action:

<div id="banner">
  <p class="site-title">
    <a href="@Url.Action("Default", "Gallery")">Photo Gallery</a>
  </p>
...
</div>

Once you’ve moved your site layout, you can begin to migrate Pages to Views. If, in your Web Pages application, you have any .cshtml pages being executed by RenderPage calls, move those either under Views/Shared for site-wide pages or into the appropriate Views sub-folder for pages shared by a controller, such as Account. Each page that calls one of these partial pages will need to be updated to reflect the new location.

All of your remaining pages should be moved under Views, organized in folders by Controller. Because your Web Pages site has no concept of a Controller, you’re going to need to introduce Controllers during a migration. The good news is that a form of Controller structure is evident in the Photo Gallery application, and illustrates a good practice to follow in your own sites.

For example, the Photo Gallery template site uses the following folders for grouping pages: Account, Gallery, Photo, Tag and User. Each folder contains pages that enable some functionality related to that grouping. For example, the Account folder contains pages for logging into and out of the site and for registering users. The Gallery folder contains a gallery listing page, a page for adding a new gallery and a page for viewing photos in a gallery. The remaining folders are organized in a similar fashion. While such a structure is not required in Web Pages sites, it does enable an easier migration to ASP.NET MVC. In this case, each folder maps nicely to a Controller and each .cshtml file to an Action and View.

Let’s start by moving the Account folder and its three pages—Login, Logout and Register—into your ASP.NET MVC application under the Views directory. In ASP.NET MVC parlance, your Pages instantly become Views by the nature of their location in the application. You’re not done, though, because your application needs a Controller and action in order to deliver those Views to the user when requested.

Introducing Controllers

By MVC convention, the fact that you have an Account folder under Views means you should have a Controller named AccountController, so our next step is to create that Controller under the Controllers folder. Simply right-click and select Add | Controller. From this empty Controller we can create action methods that will contain the logic that now resides at the top of each of the .cshtml pages we moved into our application.

We’ll address Login.cshtml first, which contains the code in Figure 1.

Figure 1 Business Logic Contained in Login.cshtml

Page.Title = "Login";
if (IsPost) {
  var email = Request["email"];
  if (email.IsEmpty()) {
    ModelState.AddError(
      "email", "You must specify an email address.");
  }
  var password = Request["password"];
  if (password.IsEmpty()) {
    ModelState.AddError(
      "password", "You must specify a password.");
  }

  if (ModelState.IsValid) {
    var rememberMe = Request["rememberMe"].AsBool();
    if (WebSecurity.Login(email, password, rememberMe)) { 
      string returnUrl = Request["returnUrl"];        
      if (!returnUrl.IsEmpty()) {
        Context.RedirectLocal(returnUrl);
      } else{
        Response.Redirect("~/");
      }
    } else {
      ModelState.AddFormError(
        "The email or password provided is incorrect.");
    }
  }
}

Notice there are two scenarios being handled here. The first is for when the user loads the login page for the first time. In this scenario, the Page sets its title and transitions directly to markup. The second scenario is contained in the IsPost conditional, and represents the logic that executes when the user completes the login form and clicks the Login button.

In ASP.NET MVC, we handle the process of delivering an empty form and accepting a form submission by creating two action methods in our Controller, one for the empty form and one to handle the submission. The first action will set the page title and return the login View, while the second will contain the logic within the IsPost conditional. These actions are contained in Figure 2. After you’ve added these two actions, delete the header code from Login.cshtml.

Figure 2 Login Controller Actions

public ActionResult Login() {
  ViewBag.Title = "Login";
  return View();
}

[HttpPost]
public ActionResult Login(string email, string password, 
  bool? rememberMe, string returnUrl) {
  if (email.IsEmpty())
    ModelState.AddModelError("email", 
      "You must specify an email address.");
  if (password.IsEmpty())
    ModelState.AddModelError("password", 
      "You must specify a password.");
  if (!ModelState.IsValid)
    return View();
  if (WebSecurity.Login(email, password, 
    rememberMe.HasValue ? rememberMe.Value : false)) {
    if (!string.IsNullOrEmpty(returnUrl))
      return Redirect(returnUrl);
    return RedirectToAction("Default", "Gallery");
  }

  ModelState.AddModelError("_FORM", 
    "The email or password provided is incorrect");
  return View();
}

There are a number of key differences to note between the original page and the resulting action methods. For starters, you’ll notice that the IsPost conditional isn’t needed. In ASP.NET MVC, we create a post action for the login page by creating a second Login action method and decorating it with the [HttpPost] attribute. Our first Login method now does nothing more than set the ViewBag.Title property and return a ViewResult, which will then look for a view page in Views/Account called Login.cshtml.

The second thing you might notice is that our Post action contains several parameters and that all of the Request calls that the original page used are gone. By putting parameters on our method that correspond to field names in our Login form (email, password and rememberMe) we can use the ASP.NET MVC default model binder to have those items passed to us as parameters to the action, which saves us from calling the Request object ourselves and makes our action logic more succinct.

Finally, there are some slight differences in how validation is handled and redirects are performed in Web Pages and ASP.NET MVC applications. In our Web Pages site, ModelState.AddError and .AddFormError are the calls we use to notify the page that we’ve encountered invalid form data. In ASP.NET MVC applications, we use ModelState.AddModelError, which is only slightly different, but a required change for all of your pages. For redirects, our Web Pages site calls Response.Redirect when re-routing the user. In ASP.NET MVC, because our Controller actions should return an ActionResult, we call return RedirectToRoute(“Default”), which yields the same result.

Once we’ve migrated the login page, we can also quickly deal with Logout.cshtml. In Web Pages, some pages may contain logic and no markup if their purpose is to perform an action and then redirect the user, as with Logout.cshtml:

@{
  WebSecurity.Logout();
  Response.Redirect("~/");
}

In ASP.NET MVC, we’ll add a Logout action that performs this work for us:

public ActionResult Logout() {
  WebSecurity.Logout();
  return RedirectToAction("Default", "Gallery");
}

Because Views represent only the visual elements of a page and no functionality, and we’ve created an action that handles logging out the user and redirecting them, we can delete the Logout.cshtml View from our application.

So far, we’ve turned our Account pages into views by copying them into the Views/Account folder, created an AccountController to handle requests to our Account pages, and implemented action methods to handle login and logout scenarios. At this point, you can build and run the site and append Account/Login to the address bar in your browser (note that the default homepage points to Gallery/Default, which we haven’t implemented yet, thus it won’t display).

The other piece of site functionality you’ll want to deal with at this point is the code and helpers you’ve contained within the App_Code directory of your Web Pages site. At the beginning of the migration, you can move this entire directory over to your ASP.NET MVC application and include it in your project. If the directory contains any code files (.cs or .vb) you can keep them in App_Code or move them elsewhere. In either case, you’ll need to change the Build Action property of each file to Compile rather than Content. If the directory contains .cshtml files with @helper method declarations, you can leave those and utilize them as is in your ASP.NET MVC application.

For the remainder of your Web Pages site, you’ll follow a similar cycle of creating a Controller for each Views folder, creating actions for each Page and moving the header code from each page into one or more actions. In no time, you should have all of your pages cleanly separated into Controller actions and Views. However, there’s still one piece of the MVC pattern we haven’t talked about yet in this article: the Model.

Migrating Data Access into Repository Classes

Your process for taking the business logic from each page and moving that logic into one or more Controller actions will be pretty straightforward, with one exception: data access. While some of your pages might be similar to the login and logout pages and contain some logic and no data access, much of your Web Pages site probably uses a database.

The Account/Register.cshtml page is one example. When the user completes the registration form and clicks Register, the page makes two database calls, illustrated in Figure 3.

Figure 3 Register.cshtml Database Logic

var db = Database.Open("PhotoGallery");
      
var user = db.QuerySingle("SELECT Email FROM 
UserProfiles WHERE LOWER(Email) = LOWER(@0)", email);
       
if (user == null) {      
  db.Execute(
    "INSERT INTO UserProfiles (Email, DisplayName, Bio) 
    VALUES (@0, @1, @2)", email, email, "");

  try {
    WebSecurity.CreateAccount(email, password);
    WebSecurity.Login(email, password);
    Response.Redirect("~/");
  } catch (System.Web.Security.MembershipCreateUserException e) {
    ModelState.AddFormError(e.ToString());
  }
} else {
  ModelState.AddFormError("Email address is already in use.");
}

First, the register page opens the PhotoGallery database and returns a WebMatrix.Data.Database object that represents the database. Then the page uses the object to look for an existing e-mail address with the value provided by the user. If the address doesn’t exist, a new UserProfile record is created and an account is created for the user using the WebSecurity membership provider.

As long as we’ve added a reference to WebMatrix.Data and set the Copy Local property to true, we can use this database logic without any changes and the site will function normally. As you’re in the midst of migration, this might be the approach you wish to take as a tactical step to keep the site functional.

In this article, however, we’re going to take things a step further and create additional objects that contain your data access, just as we’d do for an ASP.NET MVC application were we starting from scratch. There are many patterns at your disposal to separate your Controller and data-access logic. We’ll use the Repository pattern for the Photo Gallery, and by abstracting our data access into repository classes, we can encapsulate this logic and minimize the impact should we choose to add formal Model objects or an object-relational mapping (ORM) system such as the Entity Framework down the road.

We’ll start by creating a Repositories folder in our application, along with a simple class called AccountRepository.cs. Then we can step through each database call in our Register action and move that logic into our repository, as shown in Figure 4.

Figure 4 AccountRepository

public class AccountRepository {
  readonly Database _database;
  public AccountRepository() {
    database = Database.Open("PhotoGallery");
  }

  public dynamic GetAccountEmail(string email) { 
    return _database.QuerySingle(
      "SELECT Email FROM UserProfiles 
      WHERE LOWER(Email) = LOWER(@0)", email);
  }
 
  public void CreateAccount(string email) {
    _database.Execute(
      "INSERT INTO UserProfiles 
      (Email, DisplayName, Bio) VALUES (@0, @1, @2)", 
      email, email, "");
  }
}

We added the call to Database.Open to the constructor of our repository and created two methods, one for looking up an account e-mail and another for creating the account.

Notice that the return type for GetAccountEmail is dynamic. In WebMatrix.Data, many of the query methods return either dynamic or IEnumerable<dynamic>, and there’s no reason you can’t continue this practice in your repositories for as long as that practice is sustainable. 

The new Register method—using our AccountRespository—is illustrated in Figure 5.

Figure 5 Register Action Using AccountRepository

[HttpPost]
public ActionResult Register(string email, string password, 
  string confirmPassword) {

  // Check Parameters (omitted)

  if (!ModelState.IsValid)
    return View();
 
  var db = new AccountRepository();
  var user = db.GetAccountEmail(email);
 
  if (user == null) {
    db.CreateAccount(email);
 
    try {
      WebSecurity.CreateAccount(email, password);
      WebSecurity.Login(email, password);
      return RedirectToAction("Default", "Gallery");
    }
    catch (System.Web.Security.MembershipCreateUserException e) {
      ModelState.AddModelError("_FORM", e.ToString());
    }
  }
  else {
    ModelState.AddModelError("_FORM", 
      "Email address is already in use.");
  }
 
  return View();
}

Using dynamic return types is completely acceptable, and might even be wise during a migration as you get your site up and running as a full-fledged ASP.NET MVC application. You aren’t required to use strongly typed models in an ASP.NET MVC application, so you can utilize this strategy as long as you don’t need a code-driven definition of your data model. The Controller logic and Views of your dynamic-model ASP.NET MVC application will operate as normal, with one exception.

You may have noticed that, in your Web Pages application, form fields are explicitly defined using standard markup:

<input type="text" />
<input type="submit" />
...

In ASP.NET MVC, the preferred way of using form controls is by using Html helper methods such as Html.TextBox or Html.TextBoxFor because these methods use the Model passed into your View to set current values and handle form validation. If you want to use these helper methods in your Views post-migration, you’ll have to introduce strongly typed Model objects and move away from using dynamic types in your repositories, because these helper methods can’t work with dynamic models.

Preserving Site URLs

Your site URLs are important. No matter the state of your site, many external sources depend on your existing URLs—search engines, documentation, communications, test scripts and the like. Because of these dependencies, you shouldn’t arbitrarily change your URLs, even for a migration.

Consider using ASP.NET Routing to ensure existing URLs are preserved. ASP.NET Routing facilitates a request and matches it to the correct resource, in our case a Controller and Action. Web Pages uses a different routing system than ASP.NET MVC, so you’ll need to spend some time ensuring that your existing URLs are preserved.

Web Pages applications can handle URLs with and without extensions. For example, both of these URLs resolve to the same page:

http://mysite/Gallery/Default.cshtml

http://mysite/Gallery/Default

An ASP.NET MVC application, however, won’t handle the first URL using the .cshtml extension. Using extension-less URLs throughout your site will ensure that search engines and other dependent sites do the same, which will minimize the migration impact to your site. If, however, you do need to handle existing URLs with extensions, you can create routes in your ASP.NET MVC application to ensure these aren’t broken.

For example, consider the default route for our Photo Gallery application:

routes.MapRoute(
  "Default", 
  "{controller}/{action}/{id}", 
  new { controller = "Home", 
    action = "Index", id = "" } 
);

To support legacy URLs in our system, we’ll need to add additional routes to our route table above this definition in the Global.asax file. Here’s an example of one such definition:

routes.MapRoute(
  "LegacyUrl",
  "{controller}/{action}.cshtml/{id}",
  new { controller = "Gallery", 
    action = "Default", id = "" }
);

In this route entry, URLs that contain the .cshtml extension are handled and sent to the appropriate Controller and Action, assuming your existing Web Pages site structure maps cleanly to a Controller/Action structure.

When planning a migration, keep in mind that your application might require a change to the default route or even additional routes to support existing URLs. If, however, you decide to break an existing URL, be sure to include Actions to handle permanent redirects for users.

Wrapping Up

To underscore the impact of a migration from Web Pages to ASP.NET MVC, let’s take a look at the before and after structure of our Photo Gallery site. In Figure 6, you’ll see the structure of the Web Pages site in WebMatrix on the left side. The right side shows that site after a completed migration to ASP.NET MVC. Notice that, while there are differences in structure, much of the end result will feel familiar to you.

Application Layout Before and After

Figure 6 Application Layout Before and After

Today, ASP.NET developers have three framework options to choose from: Web Forms, Web Pages and ASP.NET MVC. While each has its strengths, choosing one doesn’t inhibit you from leveraging another or even migrating altogether at some point in the future. And as all three are built on top of ASP.NET, moving from one to the other should never be predicated on technical reasons. If you do choose to move, the similarities between Web Pages and ASP.NET MVC would enable you to continue to use technologies such as NuGet, Razor, Web Deploy, IIS Express and SQL Compact without modification.

If you do build your application using Web Pages and decide that a move is a good idea for you, migrating from Web Pages to ASP.NET MVC is the lowest-friction path, especially if you make some up-front design decisions in your Web Pages site to group pages in folders by functionality, use relative URLs for all resources and place all business logic at the top of each page. When the time does come for migration, you’ll find that the move from Web Pages to ASP.NET MVC is smooth and straightforward, as intended.

You can find links to many of the techniques and technologies used in this article, plus a lot more, at bit.ly/WebMatrixToMVC.


Brandon Satrom works as a senior developer evangelist for Microsoft outside of Austin, Texas. He blogs at userInexperience.com, podcasts at DeveloperSmackdown.com and can be followed on Twitter at twitter.com/BrandonSatrom.

Clark Sell works as a senior developer evangelist for Microsoft outside of Chicago. He blogs at csell.net, podcasts at DeveloperSmackdown.com and can be followed on Twitter at twitter.com/csell5.

Thanks to the following technical experts for reviewing this article: Phil Haack and Erik Porter