ASP.NET Web API and greedy query string parameter binding

Β· 618 words Β· 3 minutes to read

Following the last article on parameter binding from URI, I received an interesting question - “what if I wanted to prevent binding parameters from query string and only allow binding from route values”? In other words, prevent passing values to actions via query strings and only look for them in the route itself (to avoid duplicate ways of reaching the same endpoint).

This is possible in Web API - let’s explore how you’d go about implementing it.

More after the jump.

The problem of greedy query strings in ASP.NET Web API πŸ”—

Imagine you have a simple controller and a nice route associated with it:

public class TestController : ApiController  
{  
public string Get(string p1, string p2)  
{  
return p1 + " " + p2;  
}  
}

config.Routes.MapHttpRoute(  
name: "P1P2",  
routeTemplate: "api/test/{p1}/{p2}",  
defaults: new { controller = "Test" }  
);  

You’d expect your users to invoke the action accordingly:

/api/test/hello/world  

But if you keep the default route, this will also be possible:

/api/test?p1=hello&p2=world  

If you are an API purist, you probably want to prevent it. There is, an easy way to enforce parameter binding from Route Data only, instead of greedily looking into the query string as well.

Global πŸ”—

One option is to do it globally against your HttpConfiguration.

config.Services.Replace(typeof(ValueProviderFactory), new RouteDataValueProviderFactory());  

With this simple one liner, you replace the default setting which is using both RouteDataValueProviderFactory and QueryStringValueProviderFactory with just the first one. The latter, as the name suggests, yields the QueryStringValueProvider which is responsible for the greedy lookup into the query string.

Of course doing this change at this level, means that you now have this behavior for every single action.

Per controller πŸ”—

For a more granular control, you can look into a per-controller configuration - which would allow you to move the same setting down to a controller-level

public class RouteDataValuesOnlyAttribute : Attribute, IControllerConfiguration  
{  
public void Initialize(HttpControllerSettings controllerSettings,  
HttpControllerDescriptor controllerDescriptor)  
{  
controllerSettings.Services.Replace(typeof(ValueProviderFactory), new RouteDataValueProviderFactory());  
}  
}

[RouteDataValuesOnly]  
public class TestController : ApiController  
{  
public string Get(string p1, string p2)  
{  
return p1 + " " + p2;  
}  
}  

This way the binding from query string will not work only for this specific controller.

Per parameter πŸ”—

Finally, you can also use the ValueProvider attribute to apply a specific factory to a single attribute too.

public string Get([ValueProvider(typeof(RouteDataValueProviderFactory))]string p1,  
[ValueProvider(typeof(RouteDataValueProviderFactory))]string p2)  
{  
return p1 + " " + p2;  
}  

While this looks a little less readable, it gives you the ultimate control in terms of where Web API should look for the parameter value.

Other considerations πŸ”—

Remember, we are only enforcing policies on how the values of the parameters should be extracted in the request here.

It is important to add that action selection mechanism will still look into query string when trying to find a relevant action to execute. To circumvent this, you’d need to implement a custom IActionSelector, but that’s global for the whole API.

In other words, by applying any of the technique showed here, you should not expect that a request to /api/test?p1=hello&p2=world will be treated as a request to /api/test and routed to a different action.

Instead, the old action will still be selected - just the parameter values passed to the action will be null. Therefore, it is quite common (and sensible) to use this in conjunction with checking the parameters for null values:

public class CheckNullAttribute : ActionFilterAttribute  
{  
public override void OnActionExecuting(HttpActionContext actionContext)  
{  
if (actionContext.ActionArguments.Any(i => i.Value == null))  
{  
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest);  
}  
}  
}  

So the final solution might look like this:

[RouteDataValuesOnly]  
public class TestController : ApiController  
{  
[CheckNull]  
public string Get(string p1, string p2)  
{  
return p1 + " " + p2;  
}  
}  

Hopefully you’ll find this useful at some point.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP