Extending our JsonModel to help with model binding.
In a previous post I talked about creating a JsonModel
object to help when returning JsonResult
from an action method. This is especially useful when you need to return the same data from several action methods. The JsonModel
acts as a projection of the server-side data just as a plain MVC view model does. In the post I even showed you how to give the JsonModel
a constructor so that it could take in a server-side business object (like an Entity Framework entity) and assign the desired properties to the model object.
In this post I'm going to show you how you can use JSON model objects to help out when sending data the other direction. A JsonModel
can help out when model binding values from a request. How many times have you written an action method like this?
public ActionResult AddPerson(string firstName, string lastName, int age, DateTime dateOfBirth, string username, string password)
{
// ...
}
The ASP.NET MVC model binder is great, but sometimes these methods get a little unwieldy with so many arguments. Many MVC tutorials talk about model binding to objects with properties that match the incoming request values, but few of them talk about this with relation to JSON. As of ASP.NET MVC 3 the model binder even supports binding to JSON strings so you can use JSON.stringify()
on your client-side objects and send them over when needed.
I'm going to borrow the same two entity objects from my previous post.
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public string Gender { get; set; }
public List<Book> Books { get; set; }
}
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime PublishDate { get; set; }
public string Genre { get; set; }
public Author Author { get; set; }
}
I'm also going to use the same BookJsonModel
that I used in my previous example to act as a projection of the book entity.
public class BookJsonModel
{
public BookJsonModel(Book book)
{
this.title = book.Title;
this.publishDate = book.PublishDate;
this.genre = book.Genre;
this.authorName = book.Author.Name;
}
public string title { get; set; }
public string publishDate { get; set; }
public string genre { get; set; }
public string authorName { get; set; }
}
Notice once again that all the property names on the BookJsonModel
start lowercase. This is purposefully done to help us cross the client-server bridge without seemingly weird variable conventions. In javascript properties and instance methods usually start with lowercase letters and it would be strange for someone new to come along and see them all capitalized. They might not realize that the capitalization was just a result of the server-side object, which does capitalize the first letters of object properties, being turned into a javascript object. By making a special JSON model object we know that this object's very purpose is to be turned into JSON so we can breathe easy and lowercase the properties on it.
In my previous post I had an action method wherein I accepted a business object as an argument.
public JsonResult AddBook(Book book, string authorName)
{
var author = _dataContext.Authors.SingleOrDefault(x => x.Name == authorName);
if (author == null)
author = new Author { Name = authorName };
author.Books.Add(book);
_dataContext.SaveChanges();
return Json(new BookJsonModel(book));
}
This works great as long as the JsonModel
property names match the book entity's property names (model binder is case-insensitive). If you want to name your JsonModel
properties something different then when they get posted back from the client they will not match the properties on the book entity anymore. That's inconvenient but there is an even worse problem with this example. Let's add a Price
property to the book entity.
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime PublishDate { get; set; }
public string Genre { get; set; }
public double Price { get; set; }
public Author Author { get; set; }
}
Our new Price
property is something we don't want the client to set in this particular instance. We'll pretend this gets set on a secure page somewhere else in our application. The problem is, we don't discriminate against what the model binder will and won't bind to. Even though we don't include Price
on our BookJsonModel
, we are still in danger. What if someone got the bright idea to use something like Fiddler to craft a custom web request to our AddBook
action method? Nothing would be stopping them from manually adding a form value for "price". Then since we simply save the book instance given to you by the model binder you would be saving this illegitimate price value to the database. Gross!
Just like with regular MVC view models, JSON model objects can be used to prevent this malicious behavior. We are already creating a BookJsonModel
to pass to the client so why not use that same model object in our model binder?
public JsonResult AddBook(BookJsonModel bookJsonModel)
{
var author = _dataContext.Authors.SingleOrDefault(x => x.Name == bookJsonModel.authorName);
if (author == null)
author = new Author { Name = bookJsonModel.authorName };
var book = new Book
{
Title = bookJsonModel.title,
PublishDate = bookJsonModel.publishDate,
Genre = bookJsonModel.genre
};
author.Books.Add(book);
_dataContext.SaveChanges();
return Json(bookJsonModel);
}
Now we have solved the problem of binding directly to the business object. Instead of letting the model binder build the Book
object we build it ourselves and assign the properties to the values in the JSON model object. Notice also that we don't need to accept string authorName
as an argument anymore either because we already have that as a property on our JSON model and the model binder will automatically find it. We're almost done, but what if our Book
object had 20 properties? That would be seriously inconvenient if we had to manually assign property values in every single action method where we bind to the JSON model. To fix this we just need to add a helper method to our JSON model that will update a Book
object with values from the JSON model.
public class BookJsonModel
{
public BookJsonModel(Book book)
{
this.title = book.Title;
this.publishDate = book.PublishDate;
this.genre = book.Genre;
this.authorName = book.Author.Name;
}
public string title { get; set; }
public string publishDate { get; set; }
public string genre { get; set; }
public string authorName { get; set; }
public void UpdateBook(book)
{
book.Title = this.title;
book.PublishDate = this.publishDate;
book.Genre = this.genre;
}
}
Now we can update our action method to use this helper method instead.
public JsonResult AddBook(BookJsonModel bookJsonModel)
{
var author = _dataContext.Authors.SingleOrDefault(x => x.Name == bookJsonModel.authorName);
if (author == null)
author = new Author { Name = bookJsonModel.authorName };
var book = new Book();
bookJsonModel.UpdateBook(book);
author.Books.Add(book);
_dataContext.SaveChanges();
return Json(bookJsonModel);
}
Now you've successfully created a JSON model object that can be sent to the client and back again. Not only does it make things more convenient when multiple action methods deal with the same object, but it also naturally provides an important layer of security that prevents the model binder from binding to unwanted properties.