Sample MVC Solution
In this post I will show a sample ASP.NET MVC 2.0 project structure illustrating different concepts such as data access, user input validation and mapping between the domain and the view model. The project is still under construction but the source code is available at github.
I will illustrate the usage of the following frameworks:
- MvcContrib bringing useful extension methods and strongly typed helpers to ASP.NET MVC
- AutoMapper enabling easy mapping between the domain and the view models
- FluentValidation – a small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects
- NHibernate – a popular ORM in the .NET world
- FluentNHibernate – a statically compiled alternative to NHibernate’s standard hbm xml mapping
- Spring.NET – object container and dependency Injection framework for .NET
- Rhino.Mocks – A dynamic mock object framework for the .Net platform. It’s purpose is to ease testing by allowing the developer to create mock implementations of custom objects and verify the interactions using unit testing
Armed with this arsenal of frameworks let’s start exploring the solution structure. I’ve opted for 2 projects solution but in many real world applications more levels of abstraction could be brought to the business layer. Personally I favor to have less big assemblies rather than many small assemblies into the solution. Fewer the assemblies, faster the load time and faster the IDE. In this case particular attention should be brought to bring proper separation of concerns inside the same assembly
The domain consists of a single User class and a repository interface defining the different operations on this model:
Mapping
The next step is to define the mapping between our domain and a relational model expressed in a fluent manner:
public class UserMap : ClassMap<User> { public UserMap() { Table("users"); Id(x => x.Id, "usr_id"); Map(x => x.FirstName, "usr_firstname"); Map(x => x.LastName, "usr_lastname"); Map(x => x.Age, "usr_age"); } }
And here’s the implementation of the repository:
public class SqlUsersRepository : HibernateDaoSupport, IUsersRepository { public IEnumerable<User> GetUsers() { return HibernateTemplate.LoadAll<User>(); } public User Get(int id) { return HibernateTemplate.Get<User>(id); } public void Delete(int id) { HibernateTemplate.Delete(new User { Id = id }); } public int Save(User user) { return (int)HibernateTemplate.Save(user); } public void Update(User user) { HibernateTemplate.Update(user); } }
HibernateDaoSupport is a base class defined by the Spring Framework managing SQL transactions and NHibernate session.
Once we have implemented the data access layer we could move on to the web part. The application consists of a single RESTful UsersController allowing the standard CRUD operations with our users model. As all our views are strongly typed we shall define a view model for each view and a mapping between the domain model and this view model. In our simple case the view model will simply have the same structure as the domain model but in real world scenarios it will be a projection of the domain model for a particular view.
[Validator(typeof(UserViewModelValidator))] public class UserViewModel { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int? Age { get; set; } }
And the respective validator:
public class UserViewModelValidator : AbstractValidator<UserViewModel> { public UserViewModelValidator() { RuleFor(x => x.FirstName) .NotEmpty() .WithMessage("First name is required") .DisplayName("First name *"); RuleFor(x => x.LastName) .NotEmpty() .WithMessage("Last name is required") .DisplayName("Last name *"); } }
And mapper between the domain and view model:
public class UserMapper : IMapper { static UserMapper() { Mapper.CreateMap<User, UserViewModel>(); Mapper.CreateMap<UserViewModel, User>(); } public object Map(object source, Type sourceType, Type destinationType) { return Mapper.Map(source, sourceType, destinationType); } }
This bidirectional mapper will be used by our RESTful controller:
public class UsersController : BaseController<IUsersRepository> { public UsersController(IUsersRepository repository, IMapper userMapper) : base(repository, userMapper) { } [AutoMap(typeof(IEnumerable<User>), typeof(IEnumerable<UserViewModel>))] public ActionResult Index() { // return all users var users = Repository.GetUsers(); return View(users); } public ActionResult New() { // return an HTML form for describing a new user return View(); } [HttpPost] [AutoMap(typeof(User), typeof(UserViewModel))] public ActionResult Create(UserViewModel userView) { // create a new user if (!ModelState.IsValid) { return View("New", userView); } var user = (User)ModelMapper.Map(userView, typeof(UserViewModel), typeof(User)); Repository.Save(user); return RedirectToAction("Index", "Users"); } [AutoMap(typeof(User), typeof(UserViewModel))] public ActionResult Show(int id) { // find and return a specific user var user = Repository.Get(id); return View(user); } [AutoMap(typeof(User), typeof(UserViewModel))] public ActionResult Edit(int id) { // return an HTML form for editing a specific user var user = Repository.Get(id); return View(user); } [HttpPut] public ActionResult Update(UserViewModel userView) { // find and update a specific user if (!ModelState.IsValid) { return View("Edit", userView); } var user = (User)ModelMapper.Map(userView, typeof(UserViewModel), typeof(User)); Repository.Update(user); return RedirectToAction("Index", "Users"); } [HttpDelete] public ActionResult Destroy(int id) { // delete a specific user Repository.Delete(id); return RedirectToAction("Index", "Users"); } }
Notice the AutoMapAttribute. This is a custom attribute allowing us to automatically convert the domain model retrieved by the repository to a view model and present it to the view:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class AutoMapAttribute : ActionFilterAttribute { public Type SourceType { get; private set; } public Type DestType { get; private set; } public AutoMapAttribute(Type sourceType, Type destType) { SourceType = sourceType; DestType = destType; } public override void OnActionExecuted(ActionExecutedContext filterContext) { base.OnActionExecuted(filterContext); var controller = filterContext.Controller as IModelMapperController; if (controller == null) { return; } var model = filterContext.Controller.ViewData.Model; if (model != null && SourceType.IsAssignableFrom(model.GetType())) { var viewModel = controller.ModelMapper.Map(model, SourceType, DestType); filterContext.Controller.ViewData.Model = viewModel; } } }
The OnActionExecuted method will be called after each action method has finished executing and it will use the model passed to the view and convert it to the appropriate view model. It simply substitutes the ViewData.Model property with the appropriate view model to finally pass it to the view for rendering.
The controller follows the standard RESTful conventions for naming the action and the HTTP verbs:
URL |
HTTP Verb |
Action |
Description |
/users/index | GET | Index() | return all users |
/users/show/id | GET | Show(int id) | return a specific user |
/users/new | GET | New() | return an HTML form for creating a new user |
/users/create | POST | Create(UserViewModel userView) | create a new user |
/users/edit/id | GET | Edit(int id) | return an HTML form for editing a specific user |
/users/update | PUT | Update(UserViewModel userView) | update a specific user |
/users/destroy/id | DELETE | Destroy(int id) | delete a specific user |
Because most browsers support submitting HTML forms only using the GET and POST verbs, there’s the Html.HttpMethodOverride helper which generates a hidden field inside the form and is used by the routing engine to dispatch to the proper controller action.
Unit Tests
Unit testing our controller actions is essential. I’ve been using the excellent MVCContrib.TestHelper in conjunction with the Rhino.Mocks framework to test controllers in isolation by mocking the HTTP context. Here’s how the test logic looks like:
[TestClass] public class UsersControllerTests : TestControllerBuilder { private UsersController _sut; private IUsersRepository _repositoryStub; private IMapper _userMapperStub; public UsersControllerTests() { } private TestContext testContextInstance; /// ///Gets or sets the test context which provides ///information about and functionality for the current test run. /// public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } // Use TestInitialize to run code before running each test [TestInitialize()] public void MyTestInitialize() { _repositoryStub = MockRepository.GenerateStub<IUsersRepository>(); _userMapperStub = MockRepository.GenerateStub<IMapper>(); _sut = new UsersController(_repositoryStub, _userMapperStub); InitializeController(_sut); } [TestMethod] public void UsersController_Index() { // arrange var users = new User[0]; _repositoryStub.Stub(x => x.GetUsers()).Return(users); // act var actual = _sut.Index(); // assert actual .AssertViewRendered() .WithViewData<User[]>() .ShouldBe(users); } [TestMethod] public void UsersController_New() { // act var actual = _sut.New(); // assert actual .AssertViewRendered(); } [TestMethod] public void UsersController_Create_Invalid_Model_State() { // arrange _sut.ModelState.AddModelError("FirstName", "First name is required"); var userView = new UserViewModel(); // act var actual = _sut.Create(userView); // assert actual .AssertViewRendered() .ForView("New") .WithViewData<UserViewModel>() .ShouldBe(userView); } [TestMethod] public void UsersController_Create_Success() { // arrange var userView = new UserViewModel(); var user = new User(); _userMapperStub .Stub(x => x.Map(userView, typeof(UserViewModel), typeof(User))) .Return(user); // act var actual = _sut.Create(userView); // assert actual .AssertActionRedirect() .ToAction<UsersController>(c => c.Index()); _repositoryStub.AssertWasCalled(x => x.Save(user)); } [TestMethod] public void UsersController_Show() { // arrange var id = 1; var user = new User(); _repositoryStub.Stub(x => x.Get(id)).Return(user); // act var actual = _sut.Show(id); // assert actual .AssertViewRendered() .WithViewData<User>() .ShouldBe(user); } [TestMethod] public void UsersController_Edit() { // arrange var id = 1; var user = new User(); _repositoryStub.Stub(x => x.Get(id)).Return(user); // act var actual = _sut.Edit(id); // assert actual .AssertViewRendered() .WithViewData<User>() .ShouldBe(user); } [TestMethod] public void UsersController_Update_Invalid_Model_State() { // arrange _sut.ModelState.AddModelError("FirstName", "First name is required"); var userView = new UserViewModel(); // act var actual = _sut.Update(userView); // assert actual .AssertViewRendered() .ForView("Edit") .WithViewData<UserViewModel>() .ShouldBe(userView); } [TestMethod] public void UsersController_Update_Success() { // arrange var userView = new UserViewModel(); var user = new User(); _userMapperStub .Stub(x => x.Map(userView, typeof(UserViewModel), typeof(User))) .Return(user); // act var actual = _sut.Update(userView); // assert actual .AssertActionRedirect() .ToAction<UsersController>(c => c.Index()); _repositoryStub.AssertWasCalled(x => x.Update(user)); } [TestMethod] public void UsersController_Destroy() { // arrange var id = 1; // act var actual = _sut.Destroy(id); // assert actual .AssertActionRedirect() .ToAction<UsersController>(c => c.Index()); _repositoryStub.AssertWasCalled(x => x.Delete(id)); } }
Views
And the last but not least part of the picture are the views:
Index.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %> <%@ Import Namespace="SampleMvc.Web.Models" %> <%@ Import Namespace="SampleMvc.Web.Controllers" %> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> Index </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Index</h2> <%: Html.Grid>(Model) .Columns(column => { column.For("TableLinks").Named(""); column.For(model => model.FirstName); column.For(model => model.LastName); column.For(model => model.Age); }) %> <p> <%: Html.ActionLink>(c => c.New(), "Create New") %> </p> </asp:Content>
TableLinks.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%@ Import Namespace="SampleMvc.Web.Controllers" %> <td> <%: Html.ActionLink>(c => c.Edit(Model.Id), "Edit") %> | <%: Html.ActionLink>(c => c.Show(Model.Id), "Details") %> | <% using (Html.BeginForm>(c => c.Destroy(Model.Id))) { %> <%: Html.HttpMethodOverride(HttpVerbs.Delete) %> <input type="submit" value="Delete" /> <% } %> </td>
Edit.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %> <%@ Import Namespace="SampleMvc.Web.Controllers" %> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> Edit </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Edit</h2> <% using (Html.BeginForm>(c => c.Update(null))) {%> <%: Html.ValidationSummary(true) %> <%: Html.HttpMethodOverride(HttpVerbs.Put) %> <%: Html.HiddenFor(model => model.Id) %> <%: Html.EditorForModel() %> <p> <input type="submit" value="Save" /> </p> <% } %> <div> <%: Html.ActionLink>(c => c.Index(), "Back to List") %> </div> </asp:Content>
New.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %> <%@ Import Namespace="SampleMvc.Web.Controllers" %> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> New </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>New</h2> <% using (Html.BeginForm>(c => c.Create(null))) {%> <%: Html.ValidationSummary(true) %> <%: Html.EditorForModel() %> <p> <input type="submit" value="Create" /> </p> <% } %> <div> <%: Html.ActionLink>(c => c.Index(), "Back to List") %> </div> </asp:Content>
UserViewModel.ascx editor template
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <fieldset> <legend>Fields</legend > <div class="editor-label"> <%: Html.LabelFor(model => model.FirstName) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.FirstName) %> <%: Html.ValidationMessageFor(model => model.FirstName) %> </div> <div class="editor-label"> <%: Html.LabelFor(model => model.LastName) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.LastName) %> <%: Html.ValidationMessageFor(model => model.LastName) %> </div> <div class="editor-label"> <%: Html.LabelFor(model => model.Age) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.Age) %> <%: Html.ValidationMessageFor(model => model.Age) %> </div> </fieldset>
A very important aspect of the views is that they are all strongly typed to a view model and use only strongly typed helpers, even for generating the links. One day when Visual Studio becomes power enough you will be able to seamlessly refactor/rename a property without worrying about all those magic strings.
Further enchantments will include adding client side validation using the jQuery validate plugin in order to improve the user experience and preserve bandwidth.