I’ve got my asbestos underwear on, so flame away…
So many of us are so thrilled at seeing an official Front Controller / MVC framework for ASP.NET development that I think we’ve forgotten to ask ourselves “Is this thing any good?” In *my* opinion, if you look at the ASP.NET MVC framework as a complete framework to be used out of the box, the MVC framework isn’t very good at all. However, as a set of fairly loosely coupled classes that you can use to build a highly productive, specialized MVC stack for your specific application needs, the ASP.NET MVC framework is perfectly fine.
Basically, my issue with the MVC framework is that I think it falls far short of Ruby on Rails in terms of productivity enhancing features and testability. Specifically, I think the MVC framework – out of the box – has failed by not embracing the Rails mantras of Opinionated Software, Convention over Configuration, the DRY Principle, and Testability support. That’s okay though, because we’ve found it relatively simple to cherry pick the pieces of the MVC that we wanted (routing, some of the ActionResult classes, and the WebFormsEngine) to form the core of the MVC framework that we wanted. After deciding that we would use the MVC framework (why we chose the MVC over Rails and MonoRail is a different discussion), Chad & I immediately proceeded to develop a set of “opinions” on how we wanted our architecture to work and built up some infrastructure around the MVC to make these “opinions” become true. We’re doing a joint presentation at KaizenConf called Using and Abusing ASP.NET for Fun and Profit that’s all about our particular usage of the ASP.NET MVC framework. As a preview of that talk, and for those of you who won’t be in Austin for KaizenConf, here are the “opinions” that we’ve adopted.
Our guiding principles have been heavily influenced by Rails. We’re using Convention over Configuration as much as possible, and we’re adamant about DRY. We’ve deviated from Rails by trying to use the strengths of C# 3.0 rather than trying to make C# act like a half-assed version of Ruby. To that end, we very heavily use Expression’s, Lambda’s, Object Initializers, and Generics.
Opinions
- The “Thunderdome Principle” – All Controller methods take in one ViewModel object (or zero objects in some cases) and return a single ViewModel object (one object enters, one object leaves). The Controller classes will NEVER be directly exposed to anything related to HttpContext. Nothing makes me cry like seeing people trying to write tests that mock or stub that new IHttpContextWrapper interface. Likewise, Controller methods do not return ViewResult objects and are generally decoupled from all MVC infrastructure. We adopted this strategy very early on as a way to make Controller testing simpler mechanically. It’s definitely achieved that goal, but it’s also made the Controller code very streamlined and easy to read. We’ll explain how this works at KaizenConf.
- Controllers should have no knowledge of the View. View’s should have no knowledge of the Controller. They only share the Model class, period.
- Controllers should be thin. The only job of a Controller action is to translate the model coming in to the proper service calls and to create the output model object. All responsibilities for business logic are done by delegating to non-UI service classes. In other words, business logic does NOT go into a Controller.
- No slinging around HashTable’s, IDictionary’s, or magic strings. All of these things are error prone and make development harder than strongly typed parameter objects. Instead, we…
- …say that all navigation is done by an Expression like: <%= this.LinkToAction<HomeController>(c=>c.Index(), UserMessageKeys.MAIN_HEADER_TEXT) %>. The actual implementation is half convention, and half configuration of controller “aliases.” This choice does not lock you into making all url strings an exact representation of the controller name and method name.
- …as I said earlier, all Controller actions are invoked by passing in a single parameter object
- …allow no direct access to HttpContext collections. In the very rare case we need to access something in HttpContext, we use a wrapper interface that expresses that need in terms of our application. Dependency Injection is used to get the HttpContext wrappers to the Controller classes that need them. See this post from Jimmy Bogard for another example.
- All View’s are strongly typed View’s (ViewPage<T>, where T is the ViewModel that comes out of a Controller action). This is somewhat necessary because of the Thunderdome principle, and also enables the bullet point below…
- All html form elements are built by Expressions with our own html helpers like this: <%= this.TextBoxFor(m => m.Site.Identifier)%>. This has worked out very well because:
- It makes screen synchronization simpler by binding the element names to the actual properties of the Model object. With a little code generation, we’ve basically made the effort to synchronize data between the view, the Request.Form, and the internal Domain Model objects a miniscule part of the development effort.
- This cuts down on mistakes from misnamed elements or mangled string key values on the server side
- It’s enabled automated testing of the screens. We can address screen elements in tests by a strong typed Expression as: DropDownFor(x => x.Site.Status).HasOptionValues(Given.StatusList);
- We have access to the underlying PropertyInfo while we’re building textbox’s and dropdown’s and the like. We’re just starting to take advantage of this to intelligently set restrictions on what type’s are valid in the html elements. We’re also tying into our validation attributes to automatically mark fields that are required and set some client side validation rules on the html elements. Sooner or later, we’ll also tie in tooltip values into the html controls. And all of this comes without adding any more code on the actual View.
- Server side markup is never intermingled with client side JavaScript. It is our opinion that this all too common technique leads to unreadable code and eliminates the ability to TDD the client side JavaScript. This: callFunction(‘<%=Model.Variable%>’) is not allowed. If server side data needs to be passed to client side JavaScript, we do that by writing “var something = <% =Model.Variable%>.”
- View’s should be very simple. If you’re using an if/then statement or some sort of looping expression, you’re doing something wrong. Conditional logic belongs in the Controller or in JavaScript libraries that can be tested with QUnit. No, or minimal, logic in the View’s reduces errors by moving logic out of hard to test code and into easier to test code – and yes, I’m declaring that JavaScript is easy to unit test. Tag Soup can be avoided. We tend to move looping constructs into our own partial implementation like: <%= this.RenderPartialForEachOf(m => m.Solution.Resolutions).Using<EditResolution>()%>. In that block of code, EditResolution refers to an ASCX control, and m.Solutution.Resolutions is a property of type IList<Resolution>. This statement will iterate over the list, and render a partial view for each Resolution object.
- All Domain Model classes are identified by a single numeric property called “Id” (enforced with a layer supertype). Nothing uncommon about that opinion, but it does eliminate repetition in the code by allowing us to reuse a lot of code for finding objects and Url routing between different types of Domain Model objects.
Other Stuff We’re Doing
- We use QUnit quite extensively and I’m thrilled with it as a tool. We try to treat our JavaScript work as “real” coding and apply the same quality requirements to JavaScript as our C# code, and that means TDD. We’ve found that many things are easier to unit test and build with jQuery / QUnit rather than server side markup and NUnit.
- We do have a way to test View’s in isolation with WatiN. We can set up the input Model class in memory, then render that Model with a View and make assertions on how the View was rendered. Rails has support for this very thing, and I’ve always wanted this ability. This hasn’t been as useful as I thought it was going to be once we adopted the rules about simplicity in the View’s and started using the QUnit / jQuery combination. In order to make this ViewContext work, however, we had to forgo all usage of the built in HtmlHelper’s because of some unfortunate coupling to HttpContext.
- We’re using Fluent NHibernate for configuration. We’re about to remove some of the explicit configuration in favor of the convention based mapping. At this point, we’re completely generating the database with NHibernate’s HBM2DDL tooling. The database is just flowing out of the Domain Model for the time being (until we have a need to optimize the default DDL). We have added some conventions to Fluent NHibernate to tie our validation attributes into the DDL generation. DRY baby. The lesson we can take out of Rails success is DRY. It’s just that the shape of DRY code is a bit different in the .Net world.
- We have a custom BDD style “Context” abstract class for our Controller actions. Because of the “Thunderdome Principle,” it’s simply a matter of defining the input model, running the method, then doing state-based assertions against the output model. No HttpContext.Request.Form setup, and no scraping data out of a weakly typed ViewResult after the fact. This “Context” class also uses the StructureMap AutoMocker behind the scenes to cut down on the overhead of setting up tests. Here’s an example of that context in action testing a service class:
[TestFixture]
public class When_yanking_a_workflow_item :
ActionContext<WorkflowService, YankRequest, WorkflowActionResult>
{
private User theCurrentUser;
private Queue theOriginalQueue;
// Defines the method
public When_yanking_a_workflow_item() : base( (x, input) => x.Yank(input))
{
UseInMemoryRepository = true;
}
// Set up the context
protected override void beforeEach()
{
theCurrentUser = new User().WithId(4);
WorkflowItem theCase = new Case().WithId(1);
theOriginalQueue = new Queue{Name=“OriginalQ”}.WithId(5);
theCase.Enqueue(theOriginalQueue);
// Setup the input model
Given = new YankRequest
{
CurrentUser = this.theCurrentUser,
Item = theCase
};
}
[Test]
public void the_owner_should_be_changed_to_the_current_user()
{
// The first access of the “Output” property
// runs the Controller action and caches the return
// value of the Controller action
Output.Item.Owner.ShouldBeTheSameAs(Given.CurrentUser);
}
[Test]
public void it_should_no_longer_be_assigned_to_a_queue()
{
Output.Item.Queue.ShouldBeNull();
}
[Test]
public void the_item_and_the_queue_and_the_log_should_be_saved()
{
TheLastTransactionWasCommitted()
.TheSavedObjectsWere(Output.Item, theOriginalQueue, Output.Item.LastAction);
}
}
In Closing
Please don’t think that I’m saying that the ASP.NET MVC is bad, because it’s not. I just think that the MVC out of the box is too “bland” and wishy washy. If you’re going to use it for anything bigger than a handful of screens, I’d highly advise you to add some of your own application specific sauce. I think we can agree that Microsoft couldn’t make the MVC framework opinionated without pissing off 3/4’s of its users (just think, if they were to build in direct support for persistence they’d have to use Linq to SQL or the messy tool that shall not be named, neither of which I would consider to be acceptable). Making the MVC opinionated, and hence more productive, is going to be left to the community and to each organization.