Web API with complex array parameters
Need help with this. I have a WebAPI that can receive multiple IDs as parameters. The user can call the API using 2 routes:
First route:
api/{controller}/{action}/{ids}
ex: http://localhost/api/{controller}/{action}/id1,id2,[...],idN
Method signature
public HttpResponseMessage MyFunction(
string action,
IList<string> values)
Second route:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1,id2;type2,[...],idN;typeN
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string>> ids)
Now I need to pass a new parameter to 2 existing route. The problem is that this parameter is optional and closely related to the id value. I've made some attempt, like a method with KeyValuePair, into KeyValuePair parameter, but its results in some conflict between routes.
I need something like this:
ex: http://localhost/api/{controller}/{action}/id1;param1,id2;param2,[...],idN;paramN
http://localhost/api/{controller}/id1;type1;param1,id2;type2;param2,[...],idN;typeN;paramN
source to share
I found a solution.
I first created a class to override
KeyValuePair<string, string>
to add a third element (I know it's not a couple!). I could also use the Tuple type:
public sealed class KeyValuePair<TKey, TValue1, TValue2>
: IEquatable<KeyValuePair<TKey, TValue1, TValue2>>
To use this type with a parameter, I create
ActionFilterAttribute
to strip (";") the value from the url and create a KeyValuePair (the third element is optional)
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.ContainsKey(ParameterName))
{
var keyValuePairs = /* function to split parameters */;
actionContext.ActionArguments[ParameterName] =
keyValuePairs.Select(
x => x.Split(new[] { "," }, StringSplitOptions.None))
.Select(x => new KeyValuePair<string, string, string>(x[0], x[1], x.Length == 3 ? x[2] : string.Empty))
.ToList();
}
}
Finally, I add an action attribute filter to the controller route and change the type of the parameter:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1;param1,id2;type2,[...],idN;typeN;param3
[MyCustomFilter("ids")]
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string, string>> ids)
I could use some url parsing technique, but the ActionFilterAttribute is great and the code isn't a mess at last!
source to share
You might be able to handle this by accepting an array:
public HttpResponseMessage MyFunction(
string action,
string[] values)
Displaying the route as:
api/{controller}/{action}
And using a query string to feed the values:
GET http://server/api/Controller?values=1&values=2&values=3
source to share
Assumption: you are actually executing some command with data.
If your server payload is getting more complex than a simple route can handle, consider using the POST
http verb and send it to the server as JSON
instead of handling the uri to train it as GET
.
Another assumption: you are doing complex fetching, and GET
is idiomatically correct for a RESTful service.
Use the request, per the answer posted by @TrevorPilley
source to share
Sounds like a good scenario for a custom binder. You can process and detect your incoming data yourself and pass it to your type for use in your controller. No need to fight with built-in types.
See here .
From the page (to keep the answer on SO):
Model links
A more flexible option than a type converter is to create a custom binder model. With a model binder, you access things like the HTTP request, the action description, and the raw values from the route data.
To create a model binder, follow the IModelBinder interface. This interface defines one method, BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);
Here is an example of a linking object for GeoPoint objects.
public class GeoPointModelBinder : IModelBinder { // List of known locations. private static ConcurrentDictionary<string, GeoPoint> _locations = new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase); static GeoPointModelBinder() { _locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 }; _locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 }; _locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 }; } public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType != typeof(GeoPoint)) { return false; } ValueProviderResult val = bindingContext.ValueProvider.GetValue( bindingContext.ModelName); if (val == null) { return false; } string key = val.RawValue as string; if (key == null) { bindingContext.ModelState.AddModelError( bindingContext.ModelName, "Wrong value type"); return false; } GeoPoint result; if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result)) { bindingContext.Model = result; return true; } bindingContext.ModelState.AddModelError( bindingContext.ModelName, "Cannot convert value to Location"); return false; } } A model binder gets raw input values from a value provider. This design separates two distinct functions:
The value provider accepts an HTTP request and populates a dictionary of key-value pairs. The model binder uses this vocabulary to populate the model. The Web API Default Provider gets values from route data and query string. For example, if the URI is http: // localhost / api / values / 1? Location = 48, -122 , the value provider generates the following key-value pairs:
id = "1" location = "48,122" (I accept the default route template, which is "api / {controller} / {id}".)
The parameter name for the binding is stored in the ModelBindingContext.ModelName Property. The model binder searches the dictionary for a key with this value. If the value exists and can be converted to a GeoPoint, the model binding sets the associated value to the ModelBindingContext.Model property.
Note that the model binder is not limited to a simple type conversion. In this example, the model binder first looks in the table from known locations, and if that fails, it uses type conversion.
Setting model binding
There are several ways to set a model binding. First, you can add [ModelBinder] to the parameter.
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
You can also add the [ModelBinder] attribute to the type. The Web API will use the specified binder for all parameters of this type.
[ModelBinder(typeof(GeoPointModelBinder))] public class GeoPoint { // .... }
source to share