Tuesday, April 24, 2012

A Really Sweet MVC 3 Project Template: Part 1 – The Infrastructure

Chris Domino, Director, Enterprise Architect

After doing a few MVC 3 pet projects, I've found myself starting each new one with a copy-and-paste sprint from the last. In one code session, I can have the SQL Membership provider, error handling, Email, user registration, and Entity Framework design and integration all ready to rock. However, as anyone who can spell "TFS" knows, this copy-and-pasting is of course bad. And I'm not just talking about manually touching up namespaces or hacking connection strings; those are more annoying than harmful.

But if your new application has some other company's logo? Or favicon? Or Email template? Or masthead? Not good. Sometimes a global find and replace is magical; other times, it can create far more (and more difficult) problems than it solves. So what I decided to do is create a generic MVC web application (and the encompassing Visual Studio solution) that has everything so I can clone it and strip out what I don't need when I'm onto the next project.

To avoid yet another blog past that has a massive procedure with dozens and dozens of steps, I'm going to break this up into different sections for each component of the application "template." (I put template in quotes here because it's not really a Visual Studio template; I have code that actually clones a directory and outputs new code to more easily facilitate uniqueness of guids, give control over which file types are analyzed, etc. More information about this "Project Cloner," written by Jonathon Rupp, will be available at the end of this post's series.)

Before going through the steps I've taken to build out each project, let's start with the database. Of course, if you have no database, (nor a need for any other component of this structure) just skip over that particular skip. We're doing the database first to keep the order of operations aligned with project dependencies, so that we never have to "go back" to a project once we're done configuring it.

The Database

First things first: create a new database (named ClientB) by any means you choose. (SQL Server Management Studio is about the only time I use a designer; it's the fastest way for me to rig up my database model.) Then build out the rest of your tables, views, etc. Something I almost always have in my applications (the ones that require administrative/CMS functionality at least) is the typical hierarchical term table to model the site's taxonomy. I'll be including that here so that there's something in the data model:

       
       
 
  1. CREATE TABLE [dbo].[Term] (
  2. [TermId] UNIQUEIDENTIFIER NOT NULL,
  3. [ParentTermId] UNIQUEIDENTIFIER NULL,
  4. [Name] NVARCHAR (MAX)NOT NULL,
  5. [Description] NVARCHAR (MAX)NULL,
  6. [Weight] MONEY CONSTRAINT [DF_Term_Weight] DEFAULT ((1)) NOT NULL,
  7. [Ordinal]INT NOT NULL,
  8. [IsDeleted] BIT CONSTRAINT [DF_Term_IsDeleted] DEFAULT ((0)) NOT NULL,
  9. CONSTRAINT [PK_Term] PRIMARY KEY CLUSTERED ([TermId] ASC),
  10. CONSTRAINT [FK_Term_Term] FOREIGN KEY ([ParentTermId]) REFERENCES [dbo].[Term] ([TermId]) ON DELETE NO ACTION ON UPDATE NO ACTION
  11. );
   
       

Next, since almost ALL of my MVC apps have users and forms authentication, I install the ASP.NET SQL Server schema. Real quick:

  1. Start -> All Programs -> Microsoft Visual Studio 2010 -> Visual Studio Tools -> Visual Studio Command Prompt (2010)
  2. Run "aspnet_regsql"
  3. Click "Next"
  4. Click "Next"
  5. Select a server and a database and click "Next"
  6. Click "Next"
  7. Click "Finish"

There are slightly different versions of this infrastructure depending on which version of ASP.NET and SQL Server are installed; we'll deal with this later.

The Solution

The generic starting point for this project structure is called "ClientB" (which you might have guessed from the database name). Sub in your app name wherever you see that here. When the Architecture Council at Rightpoint came up with our project template for SharePoint work, we really flexed our creative wings and went with "ClientA" for that. Following suite, and as part of the ensuing inside joke, this mess is heretofore going to be un-intuitively pronounced "CientB." So to start off on the code side, create a new blank Visual Studio 2010 solution called ClientB.

The Visual Studio solution is made up of five projects, but you probably only need three. Technically, I guess, you can have just one and shove everything in there, but that's simply not proper. The three main ones are:

  • ClientB.Web: The MVC project itself, and home to all web assets.
  • ClientB.Data: A project that houses the EF model, and all supporting partial classes and data access logic.
  • ClientB.Database: A SQL Server 2008 Database project that contains the SQL script files to facilitate quick and easy schema comparisons among development machines and production servers.

The two more optional ones are:

  • ClientB.Common: This is the "utility" code that could be accessed by any project. Since I'm sort of a nutjob about refactoring, any line of code that might be called by different projects (namely, in the context of ClientB, Web or Data) should live here. This is also the home to any constants used by your application.
  • ClientB.Dependencies: This is the perversion of TFS to bring files into the mix with source code. Any external or third party DLLs, MSIs, or other supporting files live here so they can be referenced relatively across development machines. (Note: NuGet is the answer to this problem, but I've had issues with it not upgrading and not downloading the packages I have registered. I probably need to spend a bit more time hammering this into place, but for now, old habits will be hard to break.)

Dependencies

That said, let's start with the most optional one: ClientB.Dependencies. Add a new class library project, and delete the "class1.cs" that comes along with it. To start, just add the following file to the project: C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 3\Assemblies\System.Web.Mvc.dll. Even though this is a DLL, add it as an "Existing Item," not a reference.

This way, when we reference this (or any other) DLL in a project, we'd do it by browsing to this file, so all relative references come down whenever the latest is gotten form TFS. The goal here is to be able to get, compile, and run in a new developer's first few seconds on the project.

Common

Let's finish up with the optional projects. Add another class library named "ClientB.Common" and once again delete "class1.cs." I usually add two classes to this project: Constants.cs and Utilities.cs. Both are static and public, and, like I said, contain any and all functionality that is to be shared across multiple projects in the solution. If you or your organization has a common library, that could replace or compliment this.

There are going to be a lot of places around the application where the name of the app is displayed: page titles, Email signatures, etc. In order to avoid hardcoding the name of the app everywhere, I like to refactor that into a constant - especially when the name of the app (or even the company!) changes half way through the project. This is especially helpful to support us safely cloning this code from project to project. Here's what the constants class looks like:

       
       
 
  1. namespace ClientB.Common
  2. {
  3. public static class Constants
  4. {
  5. #region Properties
  6. public const string ApplicationNamespace = "ClientB";
  7. public const string ApplicationFriendlyName = "Client B";
  8. #endregion
  9. }
  10. }
   
       

Sending Email is a great example of logic that belongs in the Common project's Utilities class. I use two methods: one to send the Email itself, and one that wraps it for sending error Emails (when you're in a hosted environment and don't have access to logs (beyond writing to a text file)). Add a reference to System.Configuration and check out the following code in Utilities:

       
       
 
  1. public static class Utilities
  2. {
  3. public static void SendErrorEmail(string message, string location)
  4. {
  5. //compose
  6. StringBuilder sb = new StringBuilder();
  7. sb.AppendFormat("An unhandled exception has been thrown in {0}.", Constants.ApplicationFriendlyName);
  8. sb.AppendFormat("{0}Location: {1}", Environment.NewLine, location);
  9. sb.AppendFormat("{0}Error: {1}", Environment.NewLine, message);
  10. //send
  11. Utilities.SendEmail(ConfigurationManager.AppSettings["ErrorEmail"], "Administrator", "Error", sb.ToString());
  12. }
  13. public static string SendEmail(string email, string to, string subject, string body)
  14. {
  15. try
  16. {
  17. //initialization
  18. MailMessage mm = new MailMessage(string.Format("{0}<{1}>", Constants.ApplicationFriendlyName, ConfigurationManager.AppSettings["AdminEmail"]), email);
  19. //build body
  20. StringBuilder sbBody = new StringBuilder();
  21. body.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.None).ToList().ForEach(x => sbBody.AppendFormat("<p style=\"margin: 10px;\"><span style=\"font-family:Arial; color:#FFFFFF;\">{0}</span></p>", x));
  22. //build template
  23. StringBuilder sb = new StringBuilder();
  24. sb.AppendFormat("<body style=\"background:url({0}/home/background) repeat left top #CCCCCC;\">", ConfigurationManager.AppSettings["URL"]);
  25. sb.Append("<table>");
  26. sb.Append("<tr>");
  27. sb.Append("<td>");
  28. sb.AppendFormat("<img src=\"{0}/home/logo\" alt=\"{1}\" />", ConfigurationManager.AppSettings["URL"], Constants.ApplicationFriendlyName);
  29. sb.Append("</td>");
  30. sb.Append("</tr>");
  31. sb.Append("<tr>");
  32. sb.Append("<td style=\"background-color:#4D4D4D;\">");
  33. sb.AppendFormat("<p style=\"margin: 10px;\"><span style=\"font-family:Arial;color:#FFFFFF;\">Hello {0},</span></p>", to);
  34. sb.AppendFormat("<p style=\"margin: 10px;\"><span style=\"font-family:Arial;color:#FFFFFF;\">{0}</span></p>", sbBody.ToString());
  35. sb.Append("<p style=\"margin: 10px;\"><span style=\"font-family:Arial;color:#FFFFFF;\">Thank you!</span></p>");
  36. sb.AppendFormat("<p style=\"margin: 10px;\"><a href=\"{0}\" style=\"color:#0071BC; font-family:Arial; text-decoration:none;\" title=\"{1}\">{1}</a></p>", ConfigurationManager.AppSettings["URL"], Constants.ApplicationFriendlyName);
  37. sb.Append(">");
  38. sb.Append("</tr>");
  39. sb.Append("</table>");
  40. sb.Append("</body>");
  41. //configure email
  42. mm.IsBodyHtml = true;
  43. mm.Body = sb.ToString();
  44. mm.Subject = string.Format("{0} - {1}", Constants.ApplicationFriendlyName, subject);
  45. //send mail
  46. new SmtpClient().Send(mm);
  47. //success
  48. return string.Empty;
  49. }
  50. catch (Exception ex)