1

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?

3
  • 1
    This is a model binding issue. you can create a custom model binder. leave the array and have the binder add any single body to an array for that action Commented May 13, 2017 at 22:43
  • @Nkosi I didn't think of using a model binder, I'll have to look into this; please feel free to post this as an answer, with a bit more detail if possible. I'll let the question sit for a bit still, to gather more attention, but I'll accept if I can get a working implementation with a model binder. Commented May 15, 2017 at 23:45
  • Provided an example specific to you scenario but there is room for improvement/expansion on the original solution. Commented May 16, 2017 at 1:52

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

10 Comments

Fantastic answer! Thank you, I'll give it a shot first thing when I'm back at my desk shortly :-)
@Dan did you get a chance to confirm that this is the answer?
Funny enough, doing it literally now; just moved the relevant ticket to "In Progress" ;-)
@Dan cool. I ran it through some integration tests and everything seemed in order. Was just wondering how it worked in your environment. Already looking at ways to incorporate it into some of my current projects. This was a good question.
So, it doesn't appear to work, because any calls to ReadAsAsync after the first fail; the underlying stream position is already moved. I'm trying to patch it with a single call to ReadAsStreamAsync, and subsequent calls to parse the stream.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.