I wrote about WebAPI’s parameter binding at a high level before. Here’s what’s happening under the hood. The most fundamental object for binding parameters from a request in WebAPI is a HttpParameterBinding. This binds a single parameter. The binding is created upfront and then is invoked across requests. This means the binding must be determined from static information such as the parameter’s name, type, or global config. A parameter binding has a reference to the HttpParameterDescriptor, which provides static information about the parameter from the action’s signature.
Here’s the key method on HttpParameterBinding:
public abstract Task ExecuteBindingAsync( ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken);
This is invoked on each request to perform the actual binding. It takes in the action context (which has the incoming request) and then does the binding and populates the result in the argument dictionary hanging off action context. This method returns a Task in case the binding needs to do an IO operation like read the content stream.
WebAPI has two major parameter bindings: ModelBindingParameterBinder or FormatterParameterBinder. The first uses model binding, and generally assembles the parameter from the URI. The second uses the MediaTypeFormatters to read the parameter from the content stream.
Ultimately, these are both just derived classes from HttpParameterBinding. Once WebAPI gets the binding, it just invokes the ExecuteBindingAsync method and doesn’t care about the parameter’s type, it’s name, whether it had a default value, whether it was model binding vs. formatters, etc.
However, you can always add your own. For example, suppose you want to bind action parameters of type IPrincipal to automatically go against the thread’s current principal. Clearly, this does touch the content stream or need the facilities from model binding. You could create a custom binding like so:
// Example of a binder public class PrincipalParameterBinding : HttpParameterBinding { public PrincipalParameterBinding(HttpParameterDescriptor p) : base(p) { } public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken) { IPrincipal p = Thread.CurrentPrincipal; SetValue(actionContext, p); var tsc = new TaskCompletionSource<object>(); tsc.SetResult(null); return tsc.Task; } }
The binding really could do anything. You could have custom bindings that go and pull values from a database.
Normally, you wouldn’t need to plug your own HttpParameterBinding. Most scenarios could be solved by plugging a simpler interface, like adding a formatter or model binder.
This is ultimately determined by the IActionValueBinder, which is a pluggable service. Here’s the order that the DefaultActionValueBinder looks in to get a binding. (I described an alternative binder here which has MVC like semantics.)
Look for a ParameterBindingAttribute
The highest precedence is to use a ParameterBindingAttribute, which can be places on a parameter site or a parameter type’s declaration. This lets you explicitly set the binding for a parameter.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] public abstract class ParameterBindingAttribute : Attribute { public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter); }
The virtual function here hints that this is really the base class of a hierarchy. [FromBody] and [ModelBinder] attributes both derive from [ParameterBinding]. [FromUri] derives from [ModelBinder] and just invokes model binding and constrains the inputs to be from the URI.
In our example, we could create our own custom attribute to provide PrincipalParameterBindings.
Look at the ParamterBinding Rules in the Configuration
The HttpConfiguration has a collection of binding rules. This is checked if there is no ParameterBinding attribute. Here are some examples of setting some binding rules for certain types.
HttpConfiguration config = new HttpConfiguration(); ParameterBindingRulesCollection pb = config.ParameterBindingRules; pb.Insert(typeof(IPrincipal), param => new PrincipalParameterBinding(param)); // custom binder against request pb.Insert(typeof(Location), param => param.BindWithModelBinding(new LocationModelBinder())); pb.Insert(typeof(string), param => param.BindWithFormatter(new CustomMediaFormatter()));
pb.Insert(typeof(IPrincipal), param => new PrincipalParameterBinding(param)); // custom binder against request pb.Insert(typeof(Location), param => param.BindWithModelBinding(new LocationModelBinder())); pb.Insert(typeof(string), param => param.BindWithFormatter(new CustomMediaFormatter()));
The first rule says that all IPrincipal types should be bound using our IPrincipal binder above.
The second rule says that all Location types should be bound using Model Binding, and specifically use the LocationModelBinder (which would implement IModelBinder).
The third rule says that all strings should be bound with the formatters.
Rules are executed in order and work against exact type matches.
The binding rules actually operate on a ParameterDescriptor. The Insert() methods above are just using a helper that filters based on the parameter’s type. So you could add a rule that binds on a parameter’s name, or even if the par
Setting rules lets your config describe how types should be bound, and alleviates needing to decorate every callsite with an attribute.
The configuration has some default entries in the parameter binding rules collection:
Since these are just regular entries in the rule collection, you can supersede them by inserting a broader rule in front of them. Or you can clear the collection completely.
The rules here are extremely flexible and can solve several scenarios:
Fallback to a default policy
If there is no attribute, and there is no rule that claims the parameter descriptor, than the default binder falls back to its default policy. That’s basically simple types are model bound against the URI, and complex types are read from the body using formatters.