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.