How to customize parameter binding

Web API provides some great out of box experience with parameter binding. It also provides some powerful extensibility points to allow you customize that binding process. For example, you can bind multiple parameters from uri. However, it can only bind one parameter from request content by default. If you try to mark multiple parameters with [FromBody] attribute, you will get an error. The other good example is sometimes people might want to bind a parameter from some other source instead of request. There are also occasions where some parameter might come from either request uri or request body. For all those situations,  a custom parameter binding will be your friend.

Scenario 1: How to bind a parameter not coming from request

Say you have a controller that will take an IPrincipal as an parameter. The IPrincipal is not coming from request. So your action looks like the following.

Code Snippet

  1.        [HttpGet]
  2.         public HttpResponseMessage BindPrincipal(IPrincipal principal)
  3.         {
  4.             return Request.CreateResponse(System.Net.HttpStatusCode.Accepted,
  5.                 String.Format("BindPrincipal with Principal name {0}.", principal.Identity.Name));
  6.         }

 Here is your custom parameter binding to bind a custom principal to the IPrincipal parameter. 

Code Snippet

  1.     /// <summary>
  2.     /// A Custom HttpParameterBinding to bind a complex type, e.g. IPrincipal, which is not coming from request
  3.     /// </summary>
  4.     public class PrincipalParameterBinding : HttpParameterBinding
  5.     {
  6.         public PrincipalParameterBinding(HttpParameterDescriptor desc)
  7.             : base(desc)
  8.         {
  9.         }
  10.  
  11.         public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
  12.         {
  13.             // Set the binding result here
  14.             SetValue(actionContext, new GenericPrincipal(new GenericIdentity("MyIdentity"), new string[]{"myrole"}));
  15.  
  16.             // now, we can return a completed task with no result
  17.             TaskCompletionSource<AsyncVoid> tcs = new TaskCompletionSource<AsyncVoid>();
  18.             tcs.SetResult(default(AsyncVoid));
  19.             return tcs.Task;
  20.         }
  21.  
  22.         private struct AsyncVoid
  23.         {
  24.         }
  25.     }

 

Scenario 2:  How to bind a complex type either from body or from uri

 Sometimes, server might need to handle different type of requests. Some requests have information stored in the request Uri, and others have them stored in the request body. Here is how you can write one action to handle both types of requests. Your action code might look like the following:

Code Snippet

  1.         [HttpGet]
  2.         [HttpPost]
  3.         public HttpResponseMessage BindCustomComplexTypeFromUriOrBody(TestItem item)
  4.         {
  5.             return Request.CreateResponse(System.Net.HttpStatusCode.Accepted,
  6.                 String.Format("BindCustomComplexTypeFromUriOrBody returns item.Name = {0}.", item.Name));
  7.         }

 Your custom parameter binding can delegate the work to either FromUri or FromBody.

Code Snippet

  1. /// <summary>
  2. /// A Custom HttpParameterBinding to bind a complex type either from request body or uri
  3. /// </summary>
  4. public class FromUriOrBodyParameterBinding : HttpParameterBinding
  5. {
  6.     HttpParameterBinding _defaultUriBinding;
  7.     HttpParameterBinding _defaultFormatterBinding;
  8.  
  9.     public FromUriOrBodyParameterBinding(HttpParameterDescriptor desc)
  10.         : base(desc)
  11.     {
  12.         _defaultUriBinding = new FromUriAttribute().GetBinding(desc);
  13.         _defaultFormatterBinding = new FromBodyAttribute().GetBinding(desc);
  14.     }
  15.  
  16.     public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
  17.     {
  18.         if (actionContext.Request.Content != null && actionContext.Request.Content.Headers.ContentLength > 0)
  19.         {
  20.             // we have something from the body, try that first
  21.             return _defaultFormatterBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
  22.         }
  23.         else
  24.         {
  25.             // we need to read things from uri
  26.             return _defaultUriBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
  27.         }
  28.     }
  29.  
  30. }

 

Scenario 3: How to bind a parameter with a different property Name

Due to various reasons, the request sometimes contains a different name for a property than the actual property name defined on the server. How do we get those mapped correctly? For example, here is the type definition that its property is named "Name" on the server, but the client wants to send it  using "$Name" instead.

Code Snippet

  1. public class TestItemRenameProperty
  2. {
  3.     [DataMember(Name = "$Name")]
  4.     public string Name { get; set; }
  5. }

Here is how the action could look like.

Code Snippet

  1. [HttpGet]
  2. public HttpResponseMessage BindCustomComplexTypeFromUriWithRenamedProperty(TestItemRenameProperty item)
  3. {
  4.     return Request.CreateResponse(System.Net.HttpStatusCode.Accepted,
  5.         String.Format("BindCustomComplexTypeFromUriWithRenamedProperty item.Name = {0}.", item.Name));
  6. }


Scenario 4: How to bind multiple parameters form a request body

This might be the most common case where you want to use custom parameter bindings. Out of box, one could only bind one parameter from request body. It forces user who want to bind multiple objects having to write a wrapper class to wrap all those parameters. Fortunately, you can also write a custom parameter binding to solve that problem elegantly. Here is how your action might look like, and your first name and last name are coming from request body.  

Code Snippet

  1. [HttpPost]
  2. public HttpResponseMessage PostMultipleParametersFromBody(string firstname, string lastname)
  3. {
  4.     return Request.CreateResponse(System.Net.HttpStatusCode.Accepted,
  5.         String.Format("BindCustomCoPostMultipleParametersFromBodymplexType FristName = {0}, LastName = {1}.", firstname, lastname));
  6. }

Now let us write a parameter binding to map those values from request body to firstname and lastname parameters.

Code Snippet

  1. /// <summary>
  2. /// A Custom HttpParameterBinding to bind multiple parameters from request body
  3. /// </summary>
  4. public class MultipleParameterFromBodyParameterBinding : HttpParameterBinding
  5. {
  6.     private const string MultipleBodyParameters = "MultipleBodyParameters";
  7.  
  8.     public MultipleParameterFromBodyParameterBinding(HttpParameterDescriptor descriptor)
  9.         : base(descriptor)
  10.     {
  11.     }
  12.  
  13.     public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
  14.     {
  15.         NameValueCollection col = TryReadBody(actionContext.Request);
  16.  
  17.         if (col == null)
  18.         {
  19.             throw new HttpResponseException(System.Net.HttpStatusCode.BadRequest);
  20.         }
  21.  
  22.         // Set the binding result here
  23.         SetValue(actionContext, col[Descriptor.ParameterName]);
  24.  
  25.         // now, we can return a completed task with no result
  26.         TaskCompletionSource<AsyncVoid> tcs = new TaskCompletionSource<AsyncVoid>();
  27.         tcs.SetResult(default(AsyncVoid));
  28.         return tcs.Task;
  29.     }
  30.  
  31.     NameValueCollection TryReadBody(HttpRequestMessage request)
  32.     {
  33.         object result = null;
  34.         if (!request.Properties.TryGetValue(MultipleBodyParameters, out result))
  35.         {
  36.             // parsing the string like firstname=Hongmei&lastname=Ge
  37.             NameValueCollection collection = new NameValueCollection();
  38.             result = request.Content.ReadAsFormDataAsync().Result;
  39.             request.Properties.Add(MultipleBodyParameters, result);
  40.         }
  41.  
  42.         return result as NameValueCollection;
  43.     }
  44.  
  45.     private struct AsyncVoid
  46.     {
  47.     }
  48. }

 

Final step: How to register a custom parameter binding

Now we have all those wonderful custom Parameter Bindings, how do you tell Web API that you want to use them instead of those default ones? Here is how you can do it. 

Code Snippet

  1.             // Register an action to create custom ParameterBinding
  2.             config.ParameterBindingRules.Insert(0, GetCustomParameterBinding);

 

Code Snippet

  1.      public static HttpParameterBinding GetCustomParameterBinding(HttpParameterDescriptor descriptor)
  2.      {
  3.          if (descriptor.ParameterType == typeof(IPrincipal))
  4.          {
  5.              return new PrincipalParameterBinding(descriptor);
  6.          }
  7.          else if ( descriptor.ParameterType == typeof(TestItem))
  8.          {
  9.              return new FromUriOrBodyParameterBinding(descriptor);
  10.          }
  11.          else if (descriptor.ParameterType == typeof(TestItemRenameProperty))
  12.          {
  13.              return new CustomModelBinderWithPropertyName(descriptor);
  14.          }
  15.          else if ( descriptor.ParameterType == typeof(string) )
  16.          {
  17.              return new MultipleParameterFromBodyParameterBinding(descriptor);
  18.          }
  19.          
  20.          // any other types, let the default parameter binding handle
  21.          return null;
  22.      }

 

To summarize, custom parameter binding is a powerful extensibility point which allows users to take full control of the parameter binding process. To learn more details, you can download the sample code from our aspnet codeplex site at https://aspnet.codeplex.com/SourceControl/changeset/66590b705e5b.

Happy coding with ASPNET Web API!