Injecting Values for Default MVC Model Binding

In a recent project, due to security concerns, we want to encrypt all QueryString values into a single encrypted value before placing the link in web pages, then decrypt it back to individual name/value pairs and pass them to default MVC model binder to bind (to both simple types and complex types).
Default MVC model binder reads from form data, query string, route data etc. Among those, only route data is editable. So the natural choice is to inject these decrypted values to route data collection:

HttpContext.Current.Request.RequestContext.RouteData.Values["AccountId"] = 1234;

But the key is where do we do that? It must be before the model binder kicks in.
I tried Application_BegineRequest and found it is too early. Any RouteData I set there will be wiped out in MVC’s routing set up process, where MVC initializes the route data with controller, action, and other values.
After some research I found I can implement a custom model binder (by inheriting the DefaultModelBinder) and register it in glabal.asax to replace the default one. I will have the opportunity to populate the route data before I pass the control to the default/base implementation:

public class CalvinModelBinder : DefaultModelBinder
{
 public override object BindModel(ControllerContext controllerContext, 
 ModelBindingContext bindingContext)
 {
 if( encryptionKeyFoundInQueryString )
 { 
 controllerContext.RouteData.Values["accountId"] = myDecryptedValue;
 }
 return base.BindModel(controllerContext, bindingContext);
 }
}

And register it in global.asax:

ModelBinders.Binders.DefaultBinder = new CalvinModelBinder();

However, this didn’t work. The controllerContext DID have my route data inserted, but bindingContext’s ValueProvider property didn’t have it, and this very bindingContext is what MVC’s default model binding uses to get values. I dived into the ValueProvider property (a ValueProvider collection consists of FormDataValueProvider, QueryStringDataValueProvider, JsonDataValueProvider etc.) and found that the RouteDataValueProvider keeps a local copy of the route data and doesn’t query controllerContext.RouteDate.Values.
After checking MVC source code, I found that RouteDataValueProvider constructs it’s local value list from controllerContext at a very early stage, before the model binding is even being called. Any changes to controllerContext.RouteData after that will not affect the internal list in RouteDataValueProvider.
Here is the final solution. First in global.asax, I registered a custom ValueProviderFactory:

ValueProviderFactories.Factories.Insert(0, new CalvinValueProviderFactory());

It’s very important that my own factory is registered at the first place, so that my object will be constructed before all other ValueProviders. Here is my value provider and its factory:

public class CalvinValueProviderFactory : ValueProviderFactory
{
 // this will be called when controllerContext first initialize ValueProvider property, 
 // which will call all registered factories. Make sure ours is the first one
	public override IValueProvider GetValueProvider(ControllerContext controllerContext)
	{
		return new CalvinValueProvider();
	}
}
public class CalvinValueProvider : IValueProvider
{
	public CalvinValueProvider()
	{
		if (!String.IsNullOrEmpty(HttpContext.Current.Request["calvinKey"]))
		{
			var values = HttpContext.Current.Request.RequestContext.RouteData.Values;
			values["AccountId"] = "4";
			values["page"] = 1;
			values["GroupName"] = "testgroup";
 //...
		}
	}
	   
 // still have to implement these method of this interface but we don't provide any values.
 // the whole purpose of this class to inject to route data before RouteDataValueProvider is being constructed
 // and we leave the binding to MVC's default mechanism
	public bool ContainsPrefix(string prefix)
	{
		return false;
	} 
	public ValueProviderResult GetValue(string key)
	{
		return null;
	}
}

The binding works for both simple types and complex types.
Initially I was trying to implement a complete customized value provider but the logic in ContainsPrefix() became very tricky. Injecting values to route data and having default MVC mechanism to handle it is much easier.

Advertisements
This entry was posted in ASP.NET, MVC and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s