I know this is an old question, but I had the same issue and here is what I came up with and hopefully will be useful to someone. This will allow passing JSON formatted parameters individually in request URL (GET), as one single JSON object after ? (GET) or within single JSON body object (POST). My goal was RPC-style functionality.
Created a custom attribute and parameter binding, inheriting from HttpParameterBinding:
public class JSONParamBindingAttribute : Attribute
{
}
public class JSONParamBinding : HttpParameterBinding
{
private static JsonSerializer _serializer = JsonSerializer.Create(new JsonSerializerSettings()
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc
});
public JSONParamBinding(HttpParameterDescriptor descriptor)
: base(descriptor)
{
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
JObject jobj = GetJSONParameters(actionContext.Request);
object value = null;
JToken jTokenVal = null;
if (!jobj.TryGetValue(Descriptor.ParameterName, out jTokenVal))
{
if (Descriptor.IsOptional)
value = Descriptor.DefaultValue;
else
throw new MissingFieldException("Missing parameter : " + Descriptor.ParameterName);
}
else
{
try
{
value = jTokenVal.ToObject(Descriptor.ParameterType, _serializer);
}
catch (Newtonsoft.Json.JsonException e)
{
throw new HttpParseException(String.Join("", "Unable to parse parameter: ", Descriptor.ParameterName, ". Type: ", Descriptor.ParameterType.ToString()));
}
}
// Set the binding result here
SetValue(actionContext, value);
// now, we can return a completed task with no result
TaskCompletionSource<AsyncVoid> tcs = new TaskCompletionSource<AsyncVoid>();
tcs.SetResult(default(AsyncVoid));
return tcs.Task;
}
public static HttpParameterBinding HookupParameterBinding(HttpParameterDescriptor descriptor)
{
if (descriptor.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<JSONParamBindingAttribute>().Count == 0
&& descriptor.ActionDescriptor.GetCustomAttributes<JSONParamBindingAttribute>().Count == 0)
return null;
var supportedMethods = descriptor.ActionDescriptor.SupportedHttpMethods;
if (supportedMethods.Contains(HttpMethod.Post) || supportedMethods.Contains(HttpMethod.Get))
{
return new JSONParamBinding(descriptor);
}
return null;
}
private JObject GetJSONParameters(HttpRequestMessage request)
{
JObject jobj = null;
object result = null;
if (!request.Properties.TryGetValue("ParamsJSObject", out result))
{
if (request.Method == HttpMethod.Post)
{
jobj = JObject.Parse(request.Content.ReadAsStringAsync().Result);
}
else if (request.RequestUri.Query.StartsWith("?%7B"))
{
jobj = JObject.Parse(HttpUtility.UrlDecode(request.RequestUri.Query).TrimStart('?'));
}
else
{
jobj = new JObject();
foreach (var kvp in request.GetQueryNameValuePairs())
{
jobj.Add(kvp.Key, JToken.Parse(kvp.Value));
}
}
request.Properties.Add("ParamsJSObject", jobj);
}
else
{
jobj = (JObject)result;
}
return jobj;
}
private struct AsyncVoid
{
}
}
Inject binding rule inside WebApiConfig.cs's Register method:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.ParameterBindingRules.Insert(0, JSONParamBinding.HookupParameterBinding);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
This allows for controller actions with default parameter values and mixed complexity, as such:
[JSONParamBinding]
[HttpPost, HttpGet]
public Widget DoWidgetStuff(Widget widget, int stockCount, string comment="no comment")
{
... do stuff, return Widget object
}
example post body:
{
"widget": {
"a": 1,
"b": "string",
"c": { "other": "things" }
},
"stockCount": 42,
"comment": "sample code"
}
or GET single param (needs URL encoding)
controllerPath/DoWidgetStuff?{"widget":{..},"comment":"test","stockCount":42}
or GET multiple param (needs URL encoding)
controllerPath/DoWidgetStuff?widget={..}&comment="test"&stockCount=42