Introduction
Frameworks are an essential part of programming. They help developers achieve complex tasks by presenting them with a simplified API over a more complex system. In my experience, it is possible to use a framework and be productive without giving too much thought to how it works.However, I like to understand how things work. I am interested in the choices made by the framework designers. I feel that by knowing how they are built my ability to code improves and I can work with the framework more efficiently.
In this blog post I begin my investigation of the ASP.Net MVC framework. I will start by examining one part of the framework, the model binding process. How this works and how it can be extended. I will look at how the choices made by the framework designers influence the code I write and my understanding of the framework.
How flexible is the framework
The framework designer has a tricky balancing act. A good framework is simple to understand, hides the system it is abstracting and allows for easy extension. The extension points are the API and, to create them, the framework designers have several tools to choose from. The most common are, composition, inheritance, and events. The choice they make will have a big influence on the code I end up writing.The ASP.Net MVC framework is an abstraction over HTTP requests and respsonses. It includes all three types of extension mechanisms. It has been designed to create HTML applications where the server is responsible for creating the markup which will be sent to the client. This is different from frameworks where the browser creates markup using a set of web services. The generation of HTML on the server was a guiding principle of the original design and has had the most influence on the API.
Model binding, deep within the framework
I am focusing on the model binding process which takes raw HTTP requests and creates real types which can be passed to controller actions. To understand its purpose I must first understand what ASP.Net MVC does when it handles a request:- When a HTTP request is made the routing engine picks it up and loads the relevant controller
- The controller examines the request and decides which action will handle it
- When the action has been identified the controller will delegate to the model binder to create the parameters for the action method from the request data
- When the model binder has created the objects for the action method, it checks they are valid. If they are, any validation errors are added to the controllers ModelState object
So far my application consists of a form, a view model object which will represent the input and a controller to handle the request
My controller action checks the validity of the input and will either update the statistics or return the form where MVC will display the errors for me. My FoodViewModel class will never fail validation though as the framework has no knowledge of what I consider an invalid request. To achieve that I have to implement some form of validation. One solution is to add the validation logic to controller action
My controller now checks the form data to see if anyone has entered house as their favourite food. If present, I add a my error to the ModelState collection which also sets the validity of the ModelState to false. My controller will now detect invalid requests.
The controller code above demonstrates a common mistake I see in MVC applications. Here the controller is doing too much work and the code is failing to use the extensions available in the framework. Instead, the FoodViewModel can be extended to work with the model binding process to handle the validation in a more elegant and focused manner.
Extending the validation process
There are two ways that I can augment my FoodViewModel with validation rules. Simple validation can be achieved by decorating properties with attributes like[Required]
or [StringLength]
. The model binder will detect these and assert the rules accordingly.
For more complex validation the framework designers chose composition as a way for my code to participate in validation and created the IValidatableObject interface.
This has a method called Validate which accepts a ValidationContext and returns an enumerable of ValidationResults. To show how this works I have updated FoodViewModel to implement the interface.
It implements the interface by defining the Validate method so that when the model binder runs it can ask my object to validate itself. If the FavouriteFood property contains the word "House" it returns an error message.
Coding to a contract
The IValidatableObject interface is a contract between the model binder and my view model which allows them to work together. The FoodViewModel is declaring that it can behave as an IValidatableObject. This allows the model binder to ask if it is valid.For the model binder this is a powerful tool. By defining this interface the model binder achieves two things, it can open itself up to the outside world and it can delegate the job of validation to someone else. This code demonstrates how the model binder can implement this
To mimic the process used by the model binder I use reflection to create an instance of the FoodViewModel and then cast it to an instance of IValidatableObject. If the cast succeeds I call the Validate method (to keep the example simple I pass in null for the validation context). Any errors that are returned I store in my error collection. Finally, I output all the messages to the console.
This code shows the power and simplicity of composition. The example code is focused on managing the process of collecting errors from other objects. It does not have any knowledge of how to validate an object but it uses a known contract to collect the results. The process of validation has been extracted and put in the IValidatableObject interface. This allows other code to extend the process by supplying their own implementations for the validation process. When this happens the two processes create a single process which does more than they could independently. This is the goal of composition, combining many simple objects to create a more complex one.
Conclusion
I feel that too often developers fail to think about the way a framework is intended to be used or what decisions have been made to abstract the lower level system. A typical indication of a lack of thinking is an application which recreates existing parts of the framework. Exploring the code and the API of a framework helps me to avoid this. I also expand my knowledge of how to use it efficiently and how to design my own code.Examining the model binder process has given me a greater knowledge of how ASP.Net MVC takes a HTTP request and generates an object for a controller action. Understanding this complex process allows me to work with the framework so that I can extend my code in the simplest way possible to achieve the goal of validation.
I also gain knowledge by studying how composition is used in a complex process. I am now able to apply this powerful design pattern to my own code. I feel that studying existing code is an excellent way to expand my knowledge and, to be honest, I find it fun to learn how things work.
1 comment:
Hey Keith,
IValidateableObject is a nice way to custom validate a viewmodel. I used it to check if a youtube video is really a youtube video belonging to the logged in user, that's one example.
The drawback of using that interface is that the ASP.NET MVC framework designers decided to not validate the decorations and the composition in one go. In that when a decoration fails, the entire composited validation is not called. This results in a user fixing all errors from the decoration first and only then the error from the composition are shown to the client. From a user perspective this is not good; a user wants to fix the errors and get on with it.
There are three ways to resolve this drawback:
1. Use only decoration, and implement a custom validation attribute yourself
2. Use only composition, and do all validation there
3. Use both, but call the composited validation method when the modelstate is not valid just before returning the view
Post a Comment