Is it possible to bind a model from both the Uri and Body?
For instance, given the following:
routes.MapHttpRoute(
name: \"API Default\",
rou
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 I understood you, this should work out of the box, e.g. this works for me:
[HttpPost]
public ActionResult Test(TempModel model)
{
ViewBag.Message = "Test: " + model.Id +", " + model.Name;
return View("About");
}
public class TempModel
{
public int Id { get; set; }
public string Name { get; set; }
}
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
and on the request: localhost:56329/Home/Test/22 with body:{"Name":"tool"}
I have my model's properties set accordingly to 22 and "tool".
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]);
}
Alright, I came up with a way to do it. Basically, I made an action filter which will run after the model has been populated from JSON. It will then look at the URL parameters, and set the appropriate properties on the model. Full source below:
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
public class UrlPopulatorFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var model = actionContext.ActionArguments.Values.FirstOrDefault();
if (model == null) return;
var modelType = model.GetType();
var routeParams = actionContext.ControllerContext.RouteData.Values;
foreach (var key in routeParams.Keys.Where(k => k != "controller"))
{
var prop = modelType.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
if (prop != null)
{
var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);
if (descriptor.CanConvertFrom(typeof(string)))
{
prop.SetValueFast(model, descriptor.ConvertFromString(routeParams[key] as string));
}
}
}
}
}