Continuing with my last post about the differences between Web Site projects and Web Application projects in ASP.NET, this post details how I converted a legacy ASP.NET Web Site project to an ASP.NET MVC project (a variant of a Web Application project).
Getting Started… ASP.NET MVC?
I originally intended to only convert the project to a web application. I ran into a couple scenarios during development in which I wanted the capabilities that a Web Application project offers over a Web Site. See my previous post for some of these differences.
However, when I started looking into the conversion process, I decided to jump ahead and convert to the ASP.NET MVC framework.
Why jump to ASP.NET MVC? I had been keeping an eye on the development of the ASP.NET team’s MVC framework (an alternative to the existing ASP.NET Web Forms model) and liked the direction it was headed. I had experimented with Ruby on Rails (probably the most well-known web MVC framework), and liked the clean separation of concerns, more natural model of web development (as opposed to the leaky abstractions of ASP.NET Web Forms), etc.
I knew it wasn’t realistic to convert all pages in the existing project to new MVC ones. However, since it’s possible to run regular Web Forms inside of MVC projects, this seemed like a good compromise to preserve legacy code while using the MVC paradigm for future work. The MVC project type is a specialized version of a Web Application Project, and current Web Forms page, even in an MVC project, can still be processed as usual since the underlying ASP.NET engine is the same for Web Forms and MVC applications.
Steps and Caveats
I began by installing the ASP.NET MVC installer, which installs the MVC library DLLs into the GAC as well as the ASP.NET MVC Visual Studio project types and templates, and creating a new ASP.NET MVC project. I then started copying files from the old to the new project.
There's a decent walkthrough here (the guide is for Web Application projects, but it was close to what I needed). Still, I was greeted with over 5,000 build errors after following the official directions and trying to build the project for the first time. I found I needed to take things slower and at a more granular level, transferring a few files at a time to the new project and evaluating the differences one by one.
I transferred several settings from the web site’s web.config appSettings section to settings in the Visual Studio project settings file (right click on project –> Properties –> Settings). This more closely follows the standard way of managing settings using Visual Studio project files, and also performs code generation to create strongly-typed properties automatically. These settings are still stored in the web.config file (so they can be changed while the web application is running), but they’re stored under a slightly different schema for project file properties.
The mechanisms for references to web services is different in Web Application projects versus Web Sites, but this is fairly straightforward as I just re-added all my references (although this might take time if you have many references).
I love ReSharper, but I had to shush it during the conversion process to avoid being nagged to death by premature warnings and code analysis.
Something to keep in mind is that the automatically generated designer files for Web Application project aspx pages are automatically re-generated when aspx files are saved. This means that when you adjust the namespace in the aspx file (e.g. with an Inherits="C4IFACT.App.WebForms.ExecutionPlans" attribute on the Page directive) then it is reflected in the designer file. I was getting many perturbing error messages about incorrect namespaces that were resolved by this (since the aspx page and designer files can temporarily get out of sync at times).
While there is some level of security from static typing and having your project "compiled" into a DLL, you should remember that an ASP.NET project has a DLL for all of the plain C# code supporting the application, but the aspx pages are not compiled when a project is built, but instead reference types in the project’s assembly and are executed by the ASP.NET runtime when a page is visited. This means that you might still have compile errors in aspx files that won’t be reflected in the build. To help this, you can flip a switch on your MVC project to trigger the ASP.NET compiler to compile ASP.NET markup and code inside aspx pages on project compile (see here for directions on doing this – it even works against Web Forms pages if you have some mixed in with your MVC project). However, this affects the performance of building the MVC project significantly.
Organization and Folder Structure
While trying to respect the default conventions of ASP.NET MVC, I also looked at Rob Conery's enhanced version and also the Rails folder structure.
While creating my folder structure, I thought about re-structuring parts of the old app and creating clean namespaces. I settled on this:
- The App folder contains most executable content (following the Rails convention). MVC controllers and views are here.
- The App\Helpers folder contains former App_Code files and miscellaneous utility classes.
- I have a separate project in the solution containing data access code and models, so I don’t have a strict folder for models. However, I have a ViewModels folder, where model classes tailored specifically for presenting view logic or data binding are.
- Master Pages and User Controls (Partials) are under Views\Shared.
- I wanted to put my Web Forms into the Views folder (even though they aren't true Views), but there seems to be a hard setting in ASP routing that doesn't serve anything from the Views folder, so I placed them into a separate WebForms folder. I could have also routed Web Forms pages to friendler URLs (using the routing portion of the MVC framework), but for now I kept things simple and used the physical location (e.g. “MyPage.aspx” is accessed at “www.mysite.com/app/webforms/mypage.aspx”).
- The Assemblies folder contains third-party, referenced assemblies (those not contained in the GAC). Note that things under here should be referenced in the project, but should also be set to Content and not be copied to the project's build output location so that they won't be included with the project output as content (although they will be included as necessary along with the project's built DLL in the published folder).
- The Public contains static content divided into Images, Stylesheets, and JavaScripts folders.
CodeFile Versus CodeBehind
One last point - Web Site project "CodeFile" directives should be "CodeBehind" in Web Application / MVC projects. I found a great tip in the comments from this post about possible confusion that can result when converting from Web Site to Web Application projects. Here's an excerpt from the comment:
The issue with CodeFile is that the code is compiled into its own (or directory level assembly) and so the type may not be available to another page unless the control is @Registered. If you dynamically load you pretty much can't reference the ASCX codebehind class. With WAP everything goes into a single assembly so you can reference the codebehind class from just about anywhere... The thing is that if you forget to set the proper format for WAP code the page/control will still work because ASP.NET internally doesn't care whether a project is stock or WAP so the page/control runs just fine with CodeFile.
This hit upon a frustrating problem I ran into: my user controls were being compiled into their own separate assembly because I left them using the CodeFile attribute, so when I tried to import the user control (with its custom server tag) globally via the web.config (as this tip suggests) and I just referred to what I thought was the correct namespace I had problems because the user control was being compiled automagically into a separate assembly where CodeFiles are compiled.
The "Convert to Web Application" context menu for aspx pages is supposed to handle this for you and change CodeFile directives to CodeBehind, but I had issues with several pages where this didn't work and I didn't realize it for awhile. Even still, this action doesn't handle namespaces for you and still accepts the Web Site-style "Folder1_Folder2_ClassName" unique name approach instead of conventional .Net class library "Folder1.Folder2.ClassName" namespaces (how I preferred things). Basically, adjusting namespaces properly ending up involving a lot of manual work, as simply copying files and clicking "Convert to Web Application" doesn't adjust namespaces properly even if you put the files in the conventional folders.