ASP.NET MVC Best Practices and Guidelines
I can't believe eight years have gone by since ASP.NET MVC first appeared. Today, I share some of the best practices and guidelines I've learned about ASP.NET MVC since it's release in December 2007.
ASP.NET MVC has been around for eight years and a lot of ASP.NET developers have embraced the new technology and shown what it's capable of in their own web apps.
While most developers were used to WebForms and Microsoft's stock controls (i.e. ListView and DataGrid), when developers moved to ASP.NET MVC, a number of developers were shocked because there wasn't enough meat for this to be a full-featured release.
But as time moved forward with each version, we started seeing a more thinner way of development rising to meet the challenge of users expecting fast, intuitive, maintenance-manageable, and standards-based web development with ASP.NET MVC. More controls were made to simplify development with each version, such as the WebGrid.
Over my years of ASP.NET MVC development, there were a number of developers posting content regarding MVC techniques. Some held true to this day, and some, well...needed refactoring.
Please note: Some of the guidelines I list below are based on my own experiences over the years since 2008.
Also, this will be a living document as I encounter more techniques and guidelines in my coding with MVC.
ASP.NET MVC Guidelines
- "Thin Controllers, Functional ViewModels, Fat Models"
If you think about it, controllers are nothing more than a director showing the way on where to go on how to process data. Your controller should be extremely thin (< 5 lines of code).
Why? I've seen controllers that have more than 50 lines of code in eight action. Yeah, eight methods...with minimum 50 lines of code...each.
- Is testing required?
When your controller is so small, is it really necessary to test the functionality of a one-line method?
By all means, everything else should be tested. However, when you have a certain pattern or controller technique established, testing may not be necessary and may be conceived as overkill to continue each and every one-liner method.
- Use Consistent Functionality with ActionResults
Once you process your data in the controller, you almost always are returning an ActionResult of some kind. It could be a View, a Redirect, or even a file.
But if you continue to build code inside of a controller, most of the code starts looking repetitive and you continue "making the donuts."
It gets monotonous and muddies up your controllers. So why not build a commonly-used ActionResult for the task?
- Keep Access To HttpContext in here...for now.
Action Filters are one of the two places where I see the HttpContext being used. Since the 30mb System.Web won't be available in the .NET Core, that means you won't have an HttpContext available to you.
While I wonder how they'll attach it to the Core framework, I've been trying to keep the HttpContext in two places in my code: ActionFilters and ModelBinders (discussed below).
My philosophy? The smaller the HttpContext footprint I have, the less of an impact of removing it from an application. If I have it in two concrete places, I won't have an issue switching it out later.
- DO NOT include conditional code in your Views
All of your processing should happen in your controller or models, not in your Views. Your Views are meant to receive and display data. I'm even stretching it when using a for..next loop in a View.
If you have an if..then in your View, this is considered bad practice. You may have "business logic" in your UI. Yikes!
It's best to move the conditional code into an HtmlHelper (see HtmlHelper Guidelines below) or process it in the model.
- Use Strong-Typed ViewModels where applicable
It's always a chore using ViewBag, ViewData, or TempData. Over the years, my perspective is to send POCOs (Plain old CLR objects) to your Views.
It makes handling your data a lot easier and provides a better Intellisense experience for designers and developers on the HTML side.
- Embrace and use Partials
Partials are the equivalent to WebForms' UserControls. The advantage to partials is that you can pass models into the partials making your site more modular.
- Make your ViewModels a little more functional
While passing data through your ViewModels requires a simple POCO, it doesn't mean that your ViewModels can't help you modify the data for a View.
For example, if you have an IEnumerable<Product> and you need the product Id and Title in a dropdown list, there is no reason why you couldn't add a property called ProductDropDownItems to your ViewModel to build those items for you.
It makes things a little simpler for the Views to pass the data to display a dropdown as opposed to displaying a LOT of code when building the code in the View.
- Quit Chasing Links; Use UrlHelpers as a "Table of Contents"
Place all of your UrlHelpers inside a folder or in one class with #regions to keep your urls in one place.
I wrote a CMS a long time ago and placed ActionLinks all over the place. When I had to refactor the Views and link locations, I had over 120 pages that required modifications.
Yeah...I was real happy about that. :-|
Consolidate all of your urls in one place so if you have a need to replace links on a number of pages, you can go to one place and change them instead of chasing links all over your application.
- Use a minimal amount of TagBuilders in your Html Helpers
It may seem tempting to place a simple @Html.YourAwesomeControl() on a View and have the HtmlHelper create ALL of the tags and build it programmatically, but I can assure you, this is a recipe for disaster.
I did this exact thing and it bit me in the butt. Anytime I had to modify a directory listing, it required a code change. A code change meant recompiling and redeploying the code again.
My point is I needed to dig into code instead of doing it declaratively (translation: through the HTML).
While it is great to build that kind of control where only one line is required to display it anywhere in your View, when the time for modifications arise, you need to dig into code and recompile.
Only use HtmlHelpers for conditional decisions on existing data through your view.
- When you encounter an 'if' statement in your View, move it to an HtmlHelper.
As I mentioned above in the Views guidelines, this would mean mixing your business logic/rules with a UI/presentation.
This is a no-no.
Take the 'if' out and move it into a new HtmlHelper. Or better yet, put it in the model instead, if applicable.
- DO NOT place database calls in your Html Helpers
I know this may be a common sense guideline, but I figure it's best to mention it anyways.
Your Html Helpers are made to act on data passed into the View. So whatever data the controller sent to the View, that data is what you have to work with when it arrives to the HtmlHelper.
When it leaves the controller, this means "the boat has left the dock." Confirm the controller sends ALL of the data needed to make a complete View.
- Also Keep Access To HttpContext in here...for now.
Model Binders are the second place where I keep my HttpContext accesses...and to a minimum I might add.
The Model Binders require access to the HttpContext for forms, query string parameters, cookies, etc. They require an HttpContext.
Again, I try to keep them to a minimum so I can adjust them to ASP.NET vNext when the time comes. I'll have only two places to go to for my HttpContext adjustments.
- Keep your routes as simple as possible
When debugging routes, remove as many routes as possible or just strip it down to the basic "Default" route to see how the route is being interpreted.
If you have more than 30 routes in your App_Start\RouteConfig.cs, you may have an organizational site structure issue and need to revisit your routes.
- Keep A "Route Table" Handy
At the top of the MapRoutes, I have a list of commented possible urls that can occur in the application. This gives me an "at-a-glance" view of all of my routes in my application and allows me to compare new routes with existing routes.
Looking at my commented list of possible urls makes me ask the question, "Do I need to add another route when route <blah> works just fine for that new url?"
While this is not, by far, a conclusive list of guidelines, these are the hard and fast rules that I've learned over the 8 years of ASP.NET MVC.
Here's to another 30 years of coding and learning even more (raising a beer).
I will be adding more as my MVC coding continues. If you feel there was a guideline or technique missing from this list, please let me know and I will make sure to add it.