Fat Model, Skinny Controller - Code Architecture - ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

Part III. Code Architecture

Chapter 12. Fat Model, Skinny Controller

Up to this point, the examples in this book have been applying the opposite of a fat model, skinny controller, which is a fat controller, skinny model. The term “fat” implies the presence of business logic; “skinny” implies the lack thereof. This was done to provide focus on the new features that were being shown. In fact, you may have noticed in Chapter 8 when Web API was introduced that the MVC and Web API AuthorsController contained duplicated code to fetch the list of authors.

That is a perfect example of why fat controllers are convoluted, hard to maintain, and share code between them, whereas the fat model is completely geared toward reusability of code within your application.

Implementing the fat model can be done many different ways, and the depth of organization within can be from one to many layers. This all depends on the complexity of your application.

No matter which approach you take, the end goal of the fat model is to place all of your business logic in the M of MVC. The M should be able to stand alone as a complete application (without a user interface). The V and C that interact to make it MVC can be a console application, a RESTful API, a web application, etc. It shouldn’t matter to the M.

This chapter will provide an overview of common ways to separate the concerns within your MVC application followed by an example of refactoring the two AuthorsControllers to share common business logic.

Separation of Concerns

This section will discuss common ways to separate your code within an MVC application. In this section, I will discuss layers upon layers that, depending on the size of your application, may or may not be needed. The next section will demonstrate a subset of these layers that provides a clear separation of concerns. The nice part is that as your application grows and the additional layers are required, you’ll have clear spots in which to add them to make your application more organized and easier to maintain.

Controllers

When I build a controller, I think it should perform the following roles:

Data Sanitation/Validation

A controller receives a request that often contains data, whether it is from a form or the URL. This data needs to be sanitized and validated. This is demonstrated throughout this book where the controller is checking that the ModelState is valid. However, the controller is not responsible for performing business validation. It doesn’t need to know. It simply should make sure an email is an email or a first name is populated, etc.

Convert Data for the Fat Model

Chapter 7 introduced server-side ViewModels. The actions that accepted data (Create and Edit) were updated to accept ViewModels. The fat model speaks only in data models and does not even know that ViewModels exist. It is the controller’s job to accept ViewModels from the request and convert them to data models for the business layer to execute on.

Convert Data for the View

The controller requests data from the fat model and then converts this to a ViewModel before binding the data to the View. Just like the fat model doesn’t speak in ViewModels, the View doesn’t speak in data models.

Services

I like to think of services as the middleman between controllers and the business logic. A controller calls a service to fetch data, save data, apply the business logic, etc.

This is where the layers can really start to grow, depending on the size of your application. A first stage refactoring that would provide a lot of reusability would be to move the access of the BookContext from the AuthorsController into an AuthorService. This is demonstrated in the next section.

It might be immediately evident that services are responsible for a lot of different things. This is where even more layers can be added beneath (and above) the services layer to further seperate these concerns.

Behaviors

The idea of the behavior layer is to perform as much logic as possible, whether it is simple math, complex business validation, manipulation of data, or other types of logic.

Behaviors accept models and often manipulate or validate them. If a behavior requires data, it should be provided.

By limiting the number of dependencies to your behavior, it can be extremely easy to test. By placing all (or as much as humanly possible) logic within behaviors, the complex business logic is both easy to reuse and easy to test. Both are very important factors for making your application easy to mainain.

The next section will demonstrate how behaviors are called by the service layer to perform business logic.

Repositories

The purpose of the repository layer is two-fold. The first is to place common queries that are used by multiple services in a reusable spot. The second is to remove the database framework dependency in the service layer. This allows the service layer to not concern itself with how to access the data, but just request the data it requires.

Ideally, the only thing to call a repository would be the service layer. If you adopt this layer, you might second-guess yourself when the service layer is a one-liner call to the repository because this is where it feels like an unneeded layer. The minute you have the service layer calling a repository, taking the results, and calling a behavior, it provides a more readable function because it is orchestrating the fetching of the data and the application of business logic.

Orchestrations

In a small application where there is a single entry point (e.g., a controller), the controller is often treated like an orchestrator. It is responsible for calling one service, taking the results of that, and potentially calling another service.

In the current application that is being built, orchestrations aren’t required yet because the controllers are only calling the same single service. If this were to expand, introducing the orchestration layer would make a lot of sense.

The orchestration layer allows your controller to focus on its job, which is to convert data from the request and convert data for the response.

Unit of Work

With ORMs like Entity Framework, when you query data from the database, the ORM is tracking the data in its internal context. EF uses this to know whether the data has changed and what data it needs to update when the transaction is committed. Similarly, before you commit a transaction when you are adding a new record, it needs to be added to EF’s context.

In the previous examples shown over the past few chapters, the AuthorsController marks the Author model as added, modified, or deleted. This is followed by a call to the SaveChanges function. No data is ever persisted to the database until this function is called.

Enter the Unit of Work pattern. By maintaining a single Unit of Work throughout the entire request, different services can insert or manipulate data. When the business transaction is done, the owner of the Unit of Work can commit the final transaction.

Picking the layer that owns the Unit of Work is based on complexity. You need to decide which layer knows when the final business transaction is completed. The layer that has this context is the layer that should own the Unit of Work.

For example, if you implement all of the layers, starting with a controller communicating with an orchestrator, an orchestrator communicating with one or more services, and then a service communicating with one or more repositories and one or more behaviors, the owner of the Unit of Work would then be the orchestrator. The service understands when it has finished its job, but it is unaware if there are other side effects that will be executed afterward.

In the next section, because this is a small application, I will demonstrate integrating the service and behavior layer only. In this scenario, I have deemed the service to be the owner of the Unit of Work because it knows when the business transaction is completed.

Figure 12-1 demonstrates how the Unit of Work encapsulates the entire business transaction that is owned by the orchestrator.

Figure 12-1. Unit of Work

Services and Behaviors

There has been a bit too much copying and pasting for me, and I’m starting to find the controllers to be disorganized. This section will refactor the two AuthorsControllers and split the work into one service and one behavior. The BookContext will also be completely removed from theAuthorsController and now owned by the service.

The first piece of work that needs to be refactored is the duplicated logic to get a list of authors. Example 12-1 contains the code in question.

Example 12-1. Duplicated retrieval of authors

var start = (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages =

(int)Math.Ceiling((double)db.Authors.Count() / queryOptions.PageSize);

Let’s start with the behavior. The block of code performs two different calculations. You can guarantee that if you were to build another controller that contained a list of objects, these calculations would need to be made again.

Example 12-2 creates a new class called QueryOptionsCalculator. For organization purposes, I have created a new folder called Behaviors and placed the class within it.

Example 12-2. QueryOptionsCalculator

using BootstrapIntroduction.ViewModels;

using System;

namespace BootstrapIntroduction.Behaviors

{

public class QueryOptionsCalculator

{

public static int CalculateStart(QueryOptions queryOptions)

{

return (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

}

public static int CaclulateTotalPages(int count, int pageSize)

{

return (int)Math.Ceiling((double)count / pageSize);

}

}

}

The class contains two functions, one for each calculation that was in Example 12-1.

Now it’s time for the service. Example 12-3 creates a new AuthorService class. Once again for organization purposes, I have created a new Services folder and placed this class within it.

Example 12-3. AuthorService

using BootstrapIntroduction.Behaviors;

using BootstrapIntroduction.DAL;

using BootstrapIntroduction.Models;

using BootstrapIntroduction.ViewModels;

using System;

using System.Collections.Generic;

using System.Data.Entity;

using System.Linq;

using System.Linq.Dynamic;

using System.Net;

using System.Web;

using System.Web.Mvc;

namespace BootstrapIntroduction.Services

{

public class AuthorService

{

private BookContext db = new BookContext();

public List<Author> Get(QueryOptions queryOptions)

{

var start = QueryOptionsCalculator.CalculateStart(queryOptions);

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages = QueryOptionsCalculator.CaclulateTotalPages(

db.Authors.Count(), queryOptions.PageSize);

return authors.ToList();

}

}

}

The AuthorService contains a function called Get that accepts the QueryOptions class. The two calculations are replaced with calls to the new behavior created in Example 12-2. The same query that was previously in the controllers is now done in the Get function.

To complete the removal of the BookContext from the AuthorsController, the AuthorService must implement four other functions: GetById, Insert, Update, and Delete. Also, previously the BookContext was being disposed via the controller. The AuthorService will thus implement theIDisposable interface and properly dispose of the BookContext. The dispose function of AuthorService will then be called by the AuthorsController. Example 12-4 contains the complete AuthorService.

Example 12-4. Completed AuthorService

using BootstrapIntroduction.Behaviors;

using BootstrapIntroduction.DAL;

using BootstrapIntroduction.Models;

using BootstrapIntroduction.ViewModels;

using System;

using System.Collections.Generic;

using System.Data.Entity;

using System.Linq;

using System.Linq.Dynamic;

using System.Net;

using System.Web;

using System.Web.Mvc;

namespace BootstrapIntroduction.Services

{

public class AuthorService : IDisposable

{

private BookContext db = new BookContext();

public List<Author> Get(QueryOptions queryOptions)

{

var start = QueryOptionsCalculator.CalculateStart(queryOptions);

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages = QueryOptionsCalculator.CaclulateTotalPages(

db.Authors.Count(), queryOptions.PageSize);

return authors.ToList();

}

public Author GetById(long id)

{

Author author = db.Authors.Find(id);

if (author == null)

{

throw new System.Data.Entity.Core.ObjectNotFoundException

(string.Format("Unable to find author with id {0}", id));

}

return author;

}

public void Insert(Author author)

{

db.Authors.Add(author);

db.SaveChanges();

}

public void Update(Author author)

{

db.Entry(author).State = EntityState.Modified;

db.SaveChanges();

}

public void Delete(Author author)

{

db.Authors.Remove(author);

db.SaveChanges();

}

public void Dispose() {

db.Dispose();

}

}

}

With the AuthorService completed, the two AuthorControllers can be updated to remove the BookContext and replace it with the AuthorService. Example 12-5 contains the updated MVC AuthorsController.

Example 12-5. Updated MVC AuthorsController

using System;

using System.Collections.Generic;

using System.Net;

using System.Web;

using System.Web.ModelBinding;

using System.Web.Mvc;

using BootstrapIntroduction.Models;

using BootstrapIntroduction.ViewModels;

using BootstrapIntroduction.Filters;

using BootstrapIntroduction.Services;

namespace BootstrapIntroduction.Controllers

{

public class AuthorsController : Controller

{

private AuthorService authorService;

public AuthorsController()

{

authorService = new AuthorService();

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

}

// GET: Authors

[GenerateResultListFilterAttribute(typeof(Author), typeof(AuthorViewModel))]

public ActionResult Index([Form] QueryOptions queryOptions)

{

var authors = authorService.Get(queryOptions);

ViewData["QueryOptions"] = queryOptions;

return View(authors);

}

// GET: Authors/Details/5

public ActionResult Details(int? id)

{

if (id == null)

{

return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

}

var author = authorService.GetById(id.Value);

return View(AutoMapper.Mapper.Map<Author, AuthorViewModel>(author));

}

// GET: Authors/Create

[BasicAuthorization]

public ActionResult Create()

{

return View("Form", new AuthorViewModel());

}

// GET: Authors/Edit/5

[BasicAuthorization]

public ActionResult Edit(int? id)

{

if (id == null)

{

return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

}

var author = authorService.GetById(id.Value);

return View("Form", AutoMapper.Mapper.Map<Author, AuthorViewModel>(author));

}

// GET: Authors/Delete/5

[BasicAuthorization]

public ActionResult Delete(int? id)

{

if (id == null)

{

return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

}

var author = authorService.GetById(id.Value);

return View(AutoMapper.Mapper.Map<Author, AuthorViewModel>(author));

}

// POST: Authors/Delete/5

[HttpPost, ActionName("Delete")]

[ValidateAntiForgeryToken]

[BasicAuthorization]

public ActionResult DeleteConfirmed(int id)

{

var author = authorService.GetById(id);

authorService.Delete(author);

return RedirectToAction("Index");

}

protected override void Dispose(bool disposing)

{

if (disposing)

{

authorService.Dispose();

}

base.Dispose(disposing);

}

}

}

Previously, the AuthorsController was calling Dispose on the BookContext. Like everything else in the controller that was referencing BookContext, it has been replaced with a call to dispose of the AuthorService.

Dispose

Disposing of the BookContext is important to ensure that any open database connections are properly closed at the end of each request. Leaving orphaned database connections can lead to eventual database connection problems because it is normal to allow only a limited number of concurrent connections.

There is one other small refactoring within the controllers — the automapping has been moved to the constructor instead of repeating this in each action. The fewer lines of code, the easier your application is to maintain.

Example 12-6 contains the final piece of implementing the fat model, which is to update the Web API AuthorsController.

Example 12-6. Web API AuthorsController

using System;

using System.Collections.Generic;

using System.Net;

using System.Net.Http;

using System.Web.Http;

using System.Web.Http.Description;

using BootstrapIntroduction.Models;

using BootstrapIntroduction.ViewModels;

using BootstrapIntroduction.Services;

namespace BootstrapIntroduction.Controllers.Api

{

public class AuthorsController : ApiController

{

private AuthorService authorService;

public AuthorsController()

{

authorService = new AuthorService();

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

AutoMapper.Mapper.CreateMap<AuthorViewModel, Author>();

}

// GET: api/Authors

public ResultList<AuthorViewModel> Get([FromUri]QueryOptions queryOptions)

{

var authors = authorService.Get(queryOptions);

return new ResultList<AuthorViewModel>(

AutoMapper.Mapper.Map<List<Author>, List<AuthorViewModel>>(authors)

, queryOptions);

}

// GET: api/Authors/5

[ResponseType(typeof(AuthorViewModel))]

public IHttpActionResult Get(int id)

{

var author = authorService.GetById(id);

return Ok(AutoMapper.Mapper.Map<Author, AuthorViewModel>(author));

}

// PUT: api/Authors/5

[ResponseType(typeof(void))]

public IHttpActionResult Put(AuthorViewModel author)

{

var model = AutoMapper.Mapper.Map<AuthorViewModel, Author>(author);

authorService.Update(model);

return StatusCode(HttpStatusCode.NoContent);

}

// POST: api/Authors

[ResponseType(typeof(AuthorViewModel))]

public IHttpActionResult Post(AuthorViewModel author)

{

var model = AutoMapper.Mapper.Map<AuthorViewModel, Author>(author);

authorService.Insert(model);

return CreatedAtRoute("DefaultApi", new { id = author.Id }, author);

}

// DELETE: api/Authors/5

[ResponseType(typeof(Author))]

public IHttpActionResult DeleteAuthor(int id)

{

var author = authorService.GetById(id);

authorService.Delete(author);

return Ok(author);

}

protected override void Dispose(bool disposing)

{

if (disposing)

{

authorService.Dispose();

}

base.Dispose(disposing);

}

}

}

Summary

I’m very happy with the resulting AuthorsController. Replacing the BookContext with the AuthorService has made both controllers extremely lean.

This chapter presented you with a possibility of implementing up to six layers of fat model and skinny controller. The examples implemented three of them (controller, service, and behavior). In a small project, all six layers would contain unnecessary class overhead; however, if you are working in a much larger project, you will find that you will need all six layers (and possibly more).

What I have found to be successful is to start with the minimum number of layers to separate your concerns properly. As your project evolves and grows, add the new layers as required. I’ve often added new layers only to new features and continually evolve the old code as changes are required. Quite often, you don’t have time to globally implement a new layer, but there is no harm in continuing with a structure that is no longer working for you and slowly changing it over time.