ASP.NET MVC Design Philosophy
This week the first preview of the ASP.NET MVC framework was released to the web as part of the ASP.NET 3.5 Extensions CTP Preview. It's been a few months since we started coding and as the lead developer on the project I'd like to share my thoughts on the design of this framework. This isn't a post about why MVC is great. Instead, it's a post about what we did to make MVC happen in ASP.NET.
The MVC Snake
One of the design strategies we used in the MVC framework is to have a clear sequence of stages in each request. The stages are reminiscent of the ASP.NET Page and Controls lifecycle, but much, much simpler. Here's a rough diagram of the stages from the slides that Scott Hanselman and I showed at the DevConnections conference.
When a request comes in, it goes through the stages as shown in the diagram. There are a few more stages than are shown, but they were removed so the "MVC Snake" diagram wouldn't get too cluttered. An important aspect of this diagram is that each stage only depends on the previous stages. A given stage typically doesn't make any assumptions about what follows it.
For example, the IController interface doesn't assume anything about views or any particular view factory implementation. However, views can assume that an IController was involved in their creation. The implication of this is that you can stray from our path at almost any stage and still end up with success. Want to implement a controller in some new, unique way? Go ahead!
URL Routing - Not part of MVC
Part of the MVC framework is a new URL routing mechanism. This allows you to create "friendly" and "pretty" URLs such as "http://example.org/store/categories" that map to arbitrary controller actions. There's no reason that the URL should specify a physical resource. The visitors to your site surely don't care how you architected your web site or web application.
If you've downloaded the CTP you'll notice that the URL Routing types sit in the System.Web.Mvc namespace. However, the URL Routing types aren't even a part of the MVC framework. URL Routing works with arbitrary IHttpHandlers so it can serve up arbitrary requests - not just MVC requests. For the current CTP because we just couldn't find a better place so we left the types in that namespace.
New Language Features
We're technology addicts, and it shows. The latest version of the C# language includes several new features and we decided to take advantage of them in the MVC framework. We used anonymous types in place of dictionaries, extension methods for helper methods, and lambda expressions as strongly-typed URLs.
But - and there is almost always a "but" - we went a bit too far in the current CTP. We've heard from many of you already that using anonymous types as dictionaries is cool, but hard to understand. And it doesn't have Intellisense. For the next release we're still planning on keeping those features, but we want to add more familiar ways of doing the same things. For example, we're going to let you use dictionaries when we want dictionaries. It seems obvious now. :)
Extensible, Extensible, Extensible
To build a framework that was extensible, testable, and functional, we had many important design decisions to make.
Perhaps most importantly, we went with an interface-based design. We didn't use interfaces for everything, but we did for most things. Typically we used interfaces for all behavioral types, and classes for data objects. The reason is that behavior needs to be mocked, whereas data is just data. Your data objects would probably look exactly the same as our implementations anyway, so we didn't want to bother with interfaces. There are always exceptions, and in the first CTP we may have made some decisions that don't agree with this, but we already have plans to address these for the next CTP.
Traditionally in ASP.NET, a lot of extensibility is enabled by inheriting from our classes and overriding virtual members. While it is a valid approach, it requires that you inherit from our components even if you just want to tweak one small behavior. In the MVC framework we still have many instances of that, but we also allow you to extend our functionality using composition. For example, to modify the behavior of a controller you can either derive from it or you can use a filter to externally change its behavior. The same exception I mentioned in the previous paragraph applies here.
Composition often yields better separation of concerns and allows you to more easily build cross-cutting concerns. For example, if you want to apply the same "behavior modification" to several different controller classes, you don't have to override the same virtual members in each one. You can just apply an external cross-cutting concern to the controllers you want. Thus, composition is more easily used with dependency injection frameworks.
What Does All This Lead To?
Well, what does this all lead to? Several things, of course. For example, each of the components in the MVC framework is fairly small and self contained, with single responsibilities. This means that due to their small size you have building blocks that are easier to understand. It also means that you can replace or even alter the building blocks if they don't suit your needs.
Having great amounts of extensibility means you can write unit tests for your application more easily. I remember the days when I didn't write unit tests. Dark days, they were.
But who is going to use the MVC framework? Not everyone wants to write unit tests or alter so-called building blocks. Being able to drag & drop a GridView and a SqlDataSource onto a WebForm is a valid scenario and it will continue to work. We're trying to create a better fit for ASP.NET for certain types of individuals.
Anecdotes and Miscellany
And to end with some fun facts and stories, the following is all true.
- For the released CTP, the unit test code coverage numbers were about 93%, far more than any other major feature area. This does not include the code coverage by our QA guys, which I'm sure would bring the number up to at least 99%.
- We had about 250 unit tests for MVC, and the ratio of unit test code to product code is about 1.9 to 1 (in terms of the size of the code files, not lines of code; I'm too lazy to do the latter!).
- It feels good to unit test! I can't go back to the old ways.
- Some of the code is written using true TDD, but some is code-first, but it is then quickly followed up with unit tests (prior to checking in).
- Several weeks ago we invited several customers for a top secret sneak preview of the CTP, including the MVC framework. Jeffrey Palermo was attending and trying to build some samples with the MVC framework. Sure enough, he encountered some bugs in the product. The bits were still hot off the press and some of the bits hadn't quite settled in the right place. Millions of ones and zeros - what are the odds they'd all line up correctly on the first try? Anyway, I sat with Jeffrey for about half an hour and we managed to use many of the extensibility points in the framework to work around the bugs he encountered. The whole time I kept thinking (and probably saying aloud) that I was so glad I made those methods public or virtual or whatever! Without them Jeffrey would have been stuck.
- The moral of the story: If you aren't sure, make it public. If we have to use a method, chances are, someone else does too.
Documentation and discussion forums:
Blog posts and videos by team members: