Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebApi - Bind from both Uri and Body

Is it possible to bind a model from both the Uri and Body?

For instance, given the following:

routes.MapHttpRoute(     name: "API Default",     routeTemplate: "api/{controller}/{id}",     defaults: new { id = RouteParameter.Optional } );  public class ProductsController : ApiController {     public HttpResponseMessage Put(UpdateProduct model)     {      } }  public class UpdateProduct  {     int Id { get; set;}     string Name { get; set; } } 

Is it possible to create a custom binder so that a PUT to

/api/products/1

with a JSON body of:

{     "Name": "Product Name" } 

will result in the UpdateProduct model populated with Id = 1 and Name = "Product Name"?

Update

I understand that I could change the action signature to

public HttpResponseMessage Put(int id, UpdateProduct model) {  } 

However as stated in the question, I specifically want to bind to a single model object

I have also posted this question to the WebApi Codeplex discussion forum

like image 401
kimsagro Avatar asked Jul 15 '13 02:07

kimsagro


People also ask

How do you force a Web API to read a complex type from the URI?

To force Web API to read a complex type from the URI, add the [FromUri] attribute to the parameter. The following example defines a GeoPoint type, along with a controller method that gets the GeoPoint from the URI.

What is the difference between FromBody and FromUri in Web API?

The [FromUri] attribute is prefixed to the parameter to specify that the value should be read from the URI of the request, and the [FromBody] attribute is used to specify that the value should be read from the body of the request.

Can we use FromBody with Httpget?

Please note that we are able to send [FromBody] parameter in HTTP GET Request input.


2 Answers

Here's an improved version of odyth's answer that:

  1. Works for bodiless requests too, and
  2. Gets parameters from the query string in addition to from route values.

For brevity I just post the ExecuteBindingAsyncCore method and a new auxiliary method, the rest of the class is the same.

private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,         HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger,         CancellationToken cancellationToken) {     var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken);      if(model == null) model = Activator.CreateInstance(type);      var routeDataValues = actionContext.ControllerContext.RouteData.Values;     var routeParams = routeDataValues.Except(routeDataValues.Where(v => v.Key == "controller"));     var queryStringParams = new Dictionary<string, object>(QueryStringValues(request));     var allUriParams = routeParams.Union(queryStringParams).ToDictionary(pair => pair.Key, pair => pair.Value);      foreach(var key in allUriParams.Keys) {         var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);         if(prop == null) {             continue;         }         var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);         if(descriptor.CanConvertFrom(typeof(string))) {             prop.SetValue(model, descriptor.ConvertFromString(allUriParams[key] as string));         }     }      // Set the merged model in the context     SetValue(actionContext, model);      if(BodyModelValidator != null) {         BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);     } }  private static IDictionary<string, object> QueryStringValues(HttpRequestMessage request) {     var queryString = string.Join(string.Empty, request.RequestUri.ToString().Split('?').Skip(1));     var queryStringValues = System.Web.HttpUtility.ParseQueryString(queryString);     return queryStringValues.Cast<string>().ToDictionary(x => x, x => (object)queryStringValues[x]); } 
like image 97
Konamiman Avatar answered Sep 24 '22 10:09

Konamiman


You can define your own DefaultActionValueBinder. Then you can mix and match from body and uri. Here is a blog post with an example of an MvcActionValueBinder for Web Api. Making your own DefaultActionValueBinderis a preferred solution because it guarantees the binder will have finished before any other ActionFilterAttribute are executed.

http://blogs.msdn.com/b/jmstall/archive/2012/04/18/mvc-style-parameter-binding-for-webapi.aspx

UPDATE:

I had some trouble with the implementation in the blog post and trying to get it to use my custom media formatters. Luckily all my request objects extend from a base class of Request so I made my own formatter.

in WebApiConfig

config.ParameterBindingRules.Insert(0, descriptor => descriptor.ParameterType.IsSubclassOf(typeof (Request)) ? new BodyAndUriParameterBinding(descriptor) : null); 

BodyAndUriParameterBinding.cs

public class BodyAndUriParameterBinding : HttpParameterBinding {     private IEnumerable<MediaTypeFormatter> Formatters { get; set; }     private IBodyModelValidator BodyModelValidator { get; set; }     public BodyAndUriParameterBinding(HttpParameterDescriptor descriptor)         : base (descriptor)     {         var httpConfiguration = descriptor.Configuration;         Formatters = httpConfiguration.Formatters;         BodyModelValidator = httpConfiguration.Services.GetBodyModelValidator();     }      private Task<object> ReadContentAsync(HttpRequestMessage request, Type type,         IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)     {         var content = request.Content;         if (content == null)         {             var defaultValue = MediaTypeFormatter.GetDefaultValueForType(type);             return defaultValue == null ? Task.FromResult<object>(null) : Task.FromResult(defaultValue);         }          return content.ReadAsAsync(type, formatters, formatterLogger, cancellationToken);     }      public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,         CancellationToken cancellationToken)     {         var paramFromBody = Descriptor;         var type = paramFromBody.ParameterType;         var request = actionContext.ControllerContext.Request;         var formatterLogger = new ModelStateFormatterLogger(actionContext.ModelState, paramFromBody.ParameterName);         return ExecuteBindingAsyncCore(metadataProvider, actionContext, paramFromBody, type, request, formatterLogger, cancellationToken);     }      // Perf-sensitive - keeping the async method as small as possible     private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,         HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger,         CancellationToken cancellationToken)     {         var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken);          if (model != null)         {             var routeParams = actionContext.ControllerContext.RouteData.Values;             foreach (var key in routeParams.Keys.Where(k => k != "controller"))             {                 var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);                 if (prop == null)                 {                     continue;                 }                 var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);                 if (descriptor.CanConvertFrom(typeof(string)))                 {                     prop.SetValue(model, descriptor.ConvertFromString(routeParams[key] as string));                 }             }         }          // Set the merged model in the context         SetValue(actionContext, model);          if (BodyModelValidator != null)         {             BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);         }     } } 

Request.cs

public abstract class Request : IValidatableObject {     public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)     {         yield return ValidationResult.Success;     } } 
like image 20
odyth Avatar answered Sep 23 '22 10:09

odyth