In the last post of this series we implemented a way of handling the one-to-many relationship between the Author and the Community entities, allowing the user to specify which community an Author belongs to.

Previously, you saw me adding both client-side and server-side validation to my Edit view. But the validation was very simple, in the sense that it was only checking the property types (making sure we didn’t use a string for an int value).

One of the main concepts brought to us by Dynamic Data is Data Annotations. You saw me, in a previous quick tip, use this feature to specify a rendering template for a particular field. But you can also be used for custom validation.

One of the initial question is: do I add Data Annotations to my model directly? Well, in this case, it’s not such a good idea. Our model was generated by Entity Framework and you shouldn’t touch those files. If you need to make changes to your model, EF will regenerate them for you and you will lose whatever you put in there. So, we have to do it through a mechanism called “buddy class”.

Notice that every class that is generated by EF is marked as partial. That means you can extend it in a separate file. To do this, I create a new class called Author inside my model folder with this code:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.ComponentModel.DataAnnotations;

 

namespace MVCQuickTips.Models

{

    [MetadataType(typeof(AuthorMD))]

    public partial class Author

    {

        public class AuthorMD

        {

        }

    }

}


So, I’m extending the definition of the Author class and assigning it a “buddy class” that adds some metadata (AuthorMD). We can now use this new class to assign Data Annotations to our properties:

public class AuthorMD

{

    [Required]

    [StringLength(20)]

    public string Name { get; set; }

 

    [StringLength(50,ErrorMessage="Bio can't exceed 50 characters")]

    public string Bio { get; set; }

 

    [RegularExpression(@"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$",

                        ErrorMessageResourceName = "EmailValidationMessage",

                        ErrorMessageResourceType = typeof(Resources))]

    public string Email { get; set; }

 

    [RegularExpression(@"Male|Female")]

    public string Gender { get; set; }

 

    [Range(0,1000)]

    public int ContributorPoints { get; set; }

}


Let’s take a look at what we did here. Most of the Data Annotations are pretty much self-explanatory but notice how you can specify the error message directly, or do it through a resource file if you want to use localization.

Without changing anything else, this is what will happen in my Edit view:

Edit view with custom validation

This is working for both client-side and server-side validation by the way. The client-side validation is really cool. If you start typing in the Bio field, as soon as you pass the 50 chars mark, the message will pop up, you don’t even have to tab out of the textbox.

So, we are almost at the point of making this into a “bulletproof” edit view, however there are still a couple of things we could do to improve.

One would be changing the Gender field so that it is displayed as a drop down list. In order to not drag this tip even further, please check out this previous tip where we used an Editor Template for a similar situation.

The second one is hiding the fields the user is not supposed to change such as the Id and ContributorPoints. So, I went straight to my view and removed the fields so that they no longer appear:

trimmed down edit view

Looks fine right? However, if I click the Save Button I will be presented with a nasty surprise:

ContributorPoints at zero

Notice that the ContributorPoints came down to 0. Why? Well, again it’s related to the way the Model Binder works. Let’s take a look at our code:

[HttpPost]

public ActionResult Edit(Author updatedAuthor)

{

    if (!ModelState.IsValid)

        return View(updatedAuthor);

 

    using (BulletProofDemoEntities ctx = new BulletProofDemoEntities())

    {

        var author = ctx.Authors.Include("Community").First(a => a.Id == updatedAuthor.Id);

        ctx.ApplyCurrentValues("Authors", updatedAuthor);

        author.CommunityReference.EntityKey = new System.Data.EntityKey(

                                                "BulletProofDemoEntities.Communities",

                                                "Id",

                                                Int32.Parse(Request.Form["CommunityId"]));

        ctx.SaveChanges();

        return RedirectToAction("Index");

    }

}


So our method receives an Author object that is hydrated with the form post values. Since we didn’t provide any value for the ContributorPoints object, the property will assume the default int value: zero.

This looks like poor design in the MVC framework, but actually, I’m to blame here. So I’m using the framework to do all this work for me, but then I decided to save the changes myself. The code line “ctx.ApplyCurrentValues” is asking the Entity Framework directly to apply all properties from my current object over the context, so that’s why that unwanted change goes all the way to the database.

So, let’s do it “ASP.NET MVC Style”:

[HttpPost]

public ActionResult Edit(Author updatedAuthor)

{

    if (!ModelState.IsValid)

        return View(updatedAuthor);

 

    using (BulletProofDemoEntities ctx = new BulletProofDemoEntities())

    {

        var author = ctx.Authors.Include("Community").First(a => a.Id == updatedAuthor.Id);

 

        UpdateModel(author);

 

        author.CommunityReference.EntityKey = new System.Data.EntityKey(

                                                "BulletProofDemoEntities.Communities",

                                                "Id",

                                                Int32.Parse(Request.Form["CommunityId"]));

        ctx.SaveChanges();

        return RedirectToAction("Index");

    }

}


The UpdateModel takes the values provided in the form and tries to write them into the object passed in as an argument. There is also a TryUpdateModel which will return a boolean specifying if any validation or mismatch types errors occurred (UpdateModel will simply throw an exception). In this case, since we already validated our input, we can safely assume it will work.

So now we’re finally safe…or are we? Let’s try and “hack” this application:

Notice that I manually added a parameter to the url, specifying a value for the ContributorPoints property. If I just go ahead and click Save, this will be the result:

This is definitely looking like a security flaw in our application. The Save button simply posts to the same url, so our injected parameter will be picked up by the Model Binder and find its way into our database.

There are several ways through which you can protect yourself against this problem. A rather simple way is to create a “whitelist” of properties you want to change in this method, and passing it as a parameter to your UpdateModel method:

UpdateModel(author,new string[] {"Name","Bio","Gender","Email"});


There are several other alternatives and all of them have their ups and downs. Instead of detailing them here, I’ll just refer you to this post by Justin Etheredge:

http://www.codethinked.com/post/2009/01/08/ASPNET-MVC-Think-Before-You-Bind.aspx

In this post he details several alternatives to this method, and on the comments section several other people suggest other approaches as well. So, it’s a nice read.

Ok, I think it’s time to wrap it up, it’s been a rather long Quick Tip. I know the title is a bit ambitious and I’d like to clarify that I don’t mean to presume this is really bulletproof. One of the obvious things, for developers who are actively working in ASP.NET MVC already, is that I’m not protecting my site against CSRF attacks (I’ll try to write about it later on).

However, I believe the objective of this tip was reached: tackling some common issues you’ll find as you start developing in ASP.NET MVC and raise awareness to a couple of obscure details that you might miss.

Have fun!