Reputation: 20592
The tl;dr version is: Can I emulate params
/overloading for Web API methods without having to implement a custom IHttpActionSelector?
I was surprised to find that params
isn't supported in Web API methods (and have since opened an issue in probably the wrong place)
[HttpPost]
[Route("Test")]
public IHttpActionResult Test([FromBody] params Int32[] values) {
// ...
}
POST
-ing a payload of [1,2,3]
works as expected, but simply 4
results in values
being null
.
So I decided to try method overloading instead. That, however, doesn't work either.
[HttpPost]
[Route("Test")]
public IHttpActionResult Test([FromBody] Int32 value) {
return this.Test(new[] { value });
}
[HttpPost]
[Route("Test")]
public IHttpActionResult Test([FromBody] Int32[] values) {
// ...
}
Regardless of the payload this (expectedly, I suppose) throws:
Multiple actions were found that match the request: ...
It looks like I'll have to try my hand at implementing a custom IHttpActionSelector, but I'm wondering if there's any magic I've missed that I could use instead?
Upvotes: 1
Views: 155
Reputation: 247058
Can I emulate params/overloading for Web API methods without having to implement a custom
IHttpActionSelector
?
YES
This is a binding issue related to the model.
Referencing HttpParameterBinding
The following binder and attribute was created.
public class ParamsAttribute : ParameterBindingAttribute {
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter) {
//Check to make sure that it is a params array
if (parameter.ParameterType.IsArray &&
parameter.GetCustomAttributes<ParamArrayAttribute>().Count() > 0) {
return new ParamsParameterBinding(parameter);
}
return parameter.BindAsError("invalid params binding");
}
}
public class ParamsParameterBinding : HttpParameterBinding {
public ParamsParameterBinding(HttpParameterDescriptor descriptor)
: base(descriptor) {
}
public override async Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken) {
var descriptor = this.Descriptor;
var paramName = descriptor.ParameterName;
var arrayType = descriptor.ParameterType;
var elementType = arrayType.GetElementType();
try {
//can it be converted to array
var obj = await actionContext.Request.Content.ReadAsAsync(arrayType);
actionContext.ActionArguments[paramName] = obj;
return;
} catch { }
try {
//Check if single and wrap in array
var obj = await actionContext.Request.Content.ReadAsAsync(elementType);
var array = Array.CreateInstance(elementType, 1);
array.SetValue(obj, 0);
actionContext.ActionArguments[paramName] = array;
return;
} catch { }
}
}
This allowed for the following to accept both single and multiple values posted in the body of the request.
[HttpPost]
[Route("Test")]
public IHttpActionResult Test([Params] params Int32[] values) {
// ...
}
POST
-ing a payload of [1,2,3]
will work as expected, also simply with 4 will result in values
being [4]
.
The binder now respects the params
modifier, thus enabling endpoints to accept one-or-many of a given parameter. It will also work for non-primitive objects
[HttpPost]
[Route("Customers")]
public IHttpActionResult Test([Params] params Customer[] customers) {
// do important stuff
}
This could be improved further to work with any collection as a parameter to accept single or multiple values, not just params
.
Upvotes: 1