Dan Lugg
Dan Lugg

Reputation: 20592

ASP.NET Web API overloads/params

The tl;dr version is: Can I emulate params/overloading for Web API methods without having to implement a custom IHttpActionSelector?


Params

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.

Overloading

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: ...

Conclusion

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

Answers (1)

Nkosi
Nkosi

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

Related Questions