>
Blog
Book
Portfolio
Search

4/23/2012

6257 Views // 0 Comments // Not Rated

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

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:

Code Listing 1

  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:

Code Listing 2

  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:

Code Listing 3

  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)
  51. {
  52. //error
  53. return ex.ToString();
  54. }
  55. }
  56. }

That's actually as far as I'm going with this one; refactoring is specific to each project as well as each developer's tastes. The above Email methods might be a bit too specific for our purposes here, but the code I use to generate paragraph tags seems to look consistent across most Email clients, so I thought it might be useful to just include it. But remember: if you don't want it in your template, just delete it. The goal here isn't to create a rigid one-stop-shop for all web projects you'd ever do. Instead it's just a starting point; it's stuff I've had to replicate on almost all MVC work I've done.

Database

SQL Server projects were a game changer for me. Synchronizing database schemas across my laptop, desktop, other team member's machines, as well development, staging, and production servers, was always a nightmare. What was the latest version? What state is my database in? Why doesn't my application work? These are all questions easily answered by bringing your database under source control, and throwing a nice schema comparison tool into the mix for good measure. If you have any database dependencies on your project, never leave home without one of these.

First, create a SQL Server 2008 Database project, named ClientB.Database. If you have the SQL Server tools package installed from the Web Platform Installer, there's a better version of the Schema Compare tool; you'll need to upgrade your project to take advantage of it. In this case, right click your project and select "Convert to SQL Server Database project." Then *poof* you're there.

Next let's import our current schema. Right click again, and select "Schema Compare." In the first dropdown, select "Select Source..." and configure a connection to the database. Then specify "Select Target..." in the second and configure it to talk to the project we just created. When you've got that, click "Compare" at the top.

Here's where I'd like to revisit the ASP.NET SQL Server schema discrepancies that can crop up. Not only will your development machine (no doubt running the latest version of everything; possibly the betas of the next versions of everything) probably have a different version of aspnet_regsql than your production environment; (no doubt running an older version of things, which, for example, Go Daddy does) there will also be security concerns between your database project in Visual Studio and the database itself. Namely, I'm talking about importing users, roles, and schemas. In a SQL Server Data project, think of all SQL files as "executable" scripts. If you have bad TSQL in one of them, things won't compile.

So to combat this, I simply ignore slight differences between the schemas of tables and views and the code of stored procedures and database functions, and don't even bother with any SQL objects that could be related to users. Upon every comparison, clear the "Action" checkbox next to any "Type" under the "Add" node that starts with "Role," "Schema," or "User." We don't need it, and like I said above, we don't want to deal with trying to synchronize it across our different databases.

Exclude Users Roles Schemas

Really the only parts of the SQL Membership infrastructure that I use are the authentication and authorization subsystems, which is only a slice of the technology. If you're doing profiles and whatnot, there's more to consider; the basic functionality, however, works all the same across the various versions. So, despite all this hacking on the backend, no .NET code will change in the middle layer.

Schema Comparison and reconciliation is about all I use this project for, although it can do a lot more. Really the only other functionality I need here is a place to store any random data-population scripts I might find useful. I usually have something that truncates my tables so I can test new code against a clean slate; it's so specific to the model, however, that there's no use for it in this template.

The concept of the anonymous user, however, is fairly pervasive in public-facing web sites. I like to go ahead and create an actual record in the database for the anonymous user with an empty guid for its UserId. This way, instead of nullable foreign keys, every entity in my model that has a relationship with a user can have a nice tidy link to one.

While we're at it with scripting users, let's add in an administrator. If our application has users and administrative functionality, chances are we're going to need some concept of administrators. I implement this by using the SQL Membership's cousin: the Role Provider, which is a simple implementation of groups in the database.

So let's take a look at the following script (or a slight derivation of it, depending on requirements) to make sure these users exist:

Code Listing 4

  1. --initialization
  2. DECLARE @appID UNIQUEIDENTIFIER
  3. DECLARE @roleID UNIQUEIDENTIFIER
  4. DECLARE @adminID UNIQUEIDENTIFIER
  5. DECLARE @emptyGuid UNIQUEIDENTIFIER = '00000000-0000-0000-0000-000000000000'
  6. --ensure application
  7. SELECT @appID = Applicationid FROM aspnet_Applications
  8. IF @appId IS NULL
  9. BEGIN
  10. SET @appID = NEWID()
  11. INSERT INTO aspnet_Applications VALUES ('/', '/', @appid, null)
  12. END
  13. --ensure anonymous user
  14. IF NOT EXISTS (SELECT * FROM aspnet_Users WHERE UserId=@emptyGuid)
  15. BEGIN
  16. INSERT INTO aspnet_Users VALUES
  17. (@appID, @emptyGuid, 'Anonymous', 'anonymous', NULL, 1, '2012-01-12 21:10:12.000', 0)
  18. INSERT INTO aspnet_Membership VALUES
  19. (@appID, @emptyGuid, 'cWIho8hr4yIZqXeXETZfjOJbIUkP2MDpRS8wADoIdtRVRY0WYPFCj+pPfF05/ga0', 2, 'O1JsY+aBppJzFQGmojKP+A==', NULL, 'anonymous@clientb.com', 'anonymous@clientb.com', NULL, NULL, 1, 0, '2012-01-12 21:10:12.000', '2012-01-12 21:10:12.000', '2012-01-12 21:10:12.000', '1754-01-01 00:00:00.000', 0, '1754-01-01 00:00:00.000', 0, '1754-01-01 00:00:00.000', NULL)
  20. END
  21. --ensure admin user
  22. SELECT @adminID = UserId FROMaspnet_Users WHERE UserName='Admin'
  23. IF @adminID IS NULL
  24. BEGIN
  25. SET @adminID = NEWID()
  26. INSERT INTO aspnet_Users VALUES
  27. (@appID, @adminID, 'Admin', 'admin', NULL, 1, '2012-01-12 21:10:12.000', 0)
  28. INSERT INTO aspnet_Membership VALUES
  29. (@appID, @adminID, 'cWIho8hr4yIZqXeXETZfjOJbIUkP2MDpRS8wADoIdtRVRY0WYPFCj+pPfF05/ga0', 2, 'O1JsY+aBppJzFQGmojKP+A==', NULL, 'admin@clientb.com', 'admin@clientb.com', NULL, NULL, 1, 0, '2012-01-12 21:10:12.000', '2012-01-12 21:10:12.000', '2012-01-12 21:10:12.000', '1754-01-01 00:00:00.000', 0, '1754-01-01 00:00:00.000', 0, '1754-01-01 00:00:00.000', NULL)
  30. END
  31. --ensure role
  32. SELECT @roleID = RoleID FROMaspnet_Roles WHERE RoleName='Admin'
  33. IF @roleID IS NULL
  34. BEGIN
  35. SET @roleID = NEWID()
  36. INSERT INTO aspnet_Roles VALUES(@appID, @roleID, 'Admin', 'admin', NULL)
  37. END
  38. --ensure user in role
  39. IF NOT EXISTS (SELECT * FROM aspnet_UsersInRoles WHERE RoleId=@roleID AND UserId=@adminID)
  40. BEGIN
  41. INSERT INTO aspnet_UsersInRoles VALUES (@adminID, @roleID)
  42. END

Now that we have our users scripted, go ahead and execute this against the database to get those guys in there. Finally, we'll add it to our database project for safe keeping. Right click the "Scripts" folder and select "Add..." and then "Script..." In the window that pops up, make sure to select the "Script (Not in build)" template. This allows us to store arbitrary SQL code in TFS that the project doesn't attempt to "compile" upon each build. Name it "Users.sql" and we're done.

<NOTE>

You might be wondering why, after pulling all this wonderful new SQL integration technology into our project, we're still manually running scripts in SQL Management Studio and using it only for source control. It really comes down to the fact that deploying the project (which you can (and I do) turn off in Configuration Manager for one or all the build configurations) is slooooow.

Even on my home computer, which is an i7 with a million gigs of RAM, it adds a few seconds of churn to every F5 without any updates actually being made. Even though I love love love scripted deployments of any kind, the overhead required to configure this single run-once script simply isn't worth it. Script deployments to save yourself time, not create more work.

</NOTE>

Data

Next is the data layer, for which I turn to the Entity Framework to implement every time. If you want to use Linq-To-Sql or CSLA, (juuust kidding) feel free. But EF, especially the 4.0 release, has been real solid for me. So add another class library project, delete class1, and add a reference to System.Web. Once that's all set up, we can import our data model.

Add a new item to the project of type "ADO.NET Entity Data Model" named "Entities.dbmx" and complete the ensuing wizard to select your database connection (which will of course be to the database we created at the start of this process). Something I always screw up in this wizard is which setting dictates the name of the class that will represent my context. This is the screen were you specify the name of the connection string. I go with "Database" but you can use whatever you want. I feel this is a little cleaner than "[name of solution]Entities" (which is what is generated by default).

At the end of the wizard, you specify which tables, views, and procs you want imported into the model. On this screen, expand the "Tables" node and tick off "aspnet_Users," "aspnet_Membership," "aspnet_Applications," and "Terms." In most cases, you'd probably only need the users table, but depending in your style of usage for membership, it might be nice to take all three. The terms table, again, is just for example purposes.

Add Tables

EF actually does a fairly good job of coming up with singularized and pluralized names for your entities and collections of them. However, the membership tables all have that "aspnet_" prefix and are pluralized; the OCD in me cannot stand for this. If grammatical correctness, properly naming your entities, and destroying all superfluous navigation properties is a must for you as well, perform the following clean up:

aspnet_Applications

  1. Rename entity to "Application"
  2. In the F4 properties pane, set the "Entity Set Name" to "Applications"
  3. Delete all navigation properties

aspnet_Users

  1. Rename entity to "User"
  2. In the F4 properties pane, set the "Entity Set Name" to "Users"
  3. Delete the "aspnet_Applications" navigation property
  4. Rename the "aspnet_Membership" navigation property to "Membership"

aspnet_Membership

  1. Rename entity to "Membership"
  2. Delete all navigation properties

Term

  1. Rename the "Term1" navigation property to "ChildTerms"
  2. Rename the "Term2" navigation property to "ParentTerm"

The above exercise, renaming the "Term" table's navigation properties, is the main reason I included the table at all in this structure. I wanted to demonstrate that even when EF gets the name wrong, it's really easy to correct it. Maybe your brain is fine with "Term1" and "Term2" and can deal with it; mine can't. Once you have all your entities the way you want them, the next step is to start extending them.

The partial class-ed-ness of EF entities (and the context itself) is, in my opinion, one of its strongest extensibilities, and makes tasks like databinding, just, well, stupid easy. I will demonstrate this by extending the context to return the current user, and one of the entities to add a read-only property for databinding purposes.

So first, let's get the current user. Add a class called "Database" or whatever you named your entity model to the project. Here's what it'll look like:

Code Listing 5

  1. using System;
  2. using System.Web;
  3. using System.Linq;
  4. namespace ClientB.Data
  5. {
  6. public partial class Database
  7. {
  8. #region Public Methods
  9. public User GetCurrentUser()
  10. {
  11. //see if this request is anonymous
  12. if (HttpContext.Current.Request.IsAuthenticated)
  13. {
  14. //get current user
  15. return this.Users.Include("Membership").Single(u => u.UserName.Equals(HttpContext.Current.User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
  16. }
  17. else
  18. {
  19. //get anonymous user
  20. return this.Users.Include("Membership").Single(u => u.UserId.Equals(Guid.Empty));
  21. }
  22. }
  23. #endregion
  24. }
  25. }

Extending entities is just as easy. Let's say we want to display a user's Email. In the SQL Membership infrastructure, Email is actually on the aspnet_Membership table, so we'd have to join and pull it in. But if we partial class our User entity, we can add a new ready-only property, abstract that join out, and have it display nicely (in a grid, for example) along with the rest of the "native" user properties. To see this, add another class to our Data project named "User.cs" and use the following code:

Code Listing 6

  1. namespace ClientB.Data
  2. {
  3. public partial class User
  4. {
  5. #region Properties
  6. public string Email
  7. {
  8. get
  9. {
  10. //ensure relationship is loaded
  11. if (this.Membership == null)
  12. this.MembershipReference.Load();
  13. //return email
  14. return this.Membership.Email;
  15. }
  16. }
  17. #endregion
  18. }
  19. }

As long as you manage the disposal of your context properly, you can add a bit of logic to extended properties and make your UI databinding logic a lot easier.

Web

So this post turned out to be massive; I'm going to present the Web project in a separate one, since it's far and away the biggest. Doing so sort of creates a nice separation between the idea of an MVC solution, which is all of the physical tiers of a web application and an MVC site, which is just the web components. Perhaps you have a completely different approach to setting up your MVC infrastructure, and just are interested in the web bits. If that's the case, I apologize for however long you spent reading this, as your question your question won't be answered until the next post. My bad!

Read on here.

4 Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.


Loading...