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
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.
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.
Please note that we are able to send [FromBody] parameter in HTTP GET Request input.
Here's an improved version of odyth's answer that:
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]); }
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; } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With