Reputation: 1037
Need help on this one. I have a WebAPI who can receive multiple ids as parameters. The user can call the API using 2 route:
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 the 2 existing route. The problem is this parameter is optional and tightly associated with the id value. I made some attempt like a method with KeyValuePair into KeyValuePair parameter but its results in some conflict between routes.
What I need is something like that :
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
Upvotes: 4
Views: 2081
Reputation: 1037
I found a solution.
First, I created a class to override the
KeyValuePair<string, string>
type to add a third element (I know it's not really a pair!). I could have use Tuple type also:
public sealed class KeyValuePair<TKey, TValue1, TValue2>
: IEquatable<KeyValuePair<TKey, TValue1, TValue2>>
To use this type with parameter, I create an
ActionFilterAttribute
to split (";") the value from the url and create a KeyValuePair (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();
}
}
And finally, I add the action attribute filter to the controller route and change the parameter type:
"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 is not a mess finally!
Upvotes: 0
Reputation: 452
Looks like a good scenario for a custom model binder. You can handle your incoming data and detect it your self and pass it to your own type to use in your controller. No need to fight with the built in types.
See here.
From the page (to keep the answer on SO):
Model Binders
A more flexible option than a type converter is to create a custom model binder. With a model binder, you have access to things like the HTTP request, the action description, and the raw values from the route data.
To create a model binder, implement the IModelBinder interface. This interface defines a single method, BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);
Here is a model binder 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 takes the HTTP request and populates a dictionary of key-value pairs. The model binder uses this dictionary to populate the model. The default value provider in Web API gets values from the route data and the query string. For example, if the URI is http://localhost/api/values/1?location=48,-122, the value provider creates the following key-value pairs:
id = "1" location = "48,122" (I'm assuming the default route template, which is "api/{controller}/{id}".)
The name of the parameter to bind is stored in the ModelBindingContext.ModelName property. The model binder looks for a key with this value in the dictionary. If the value exists and can be converted into a GeoPoint, the model binder assigns the bound value to the ModelBindingContext.Model property.
Notice that the model binder is not limited to a simple type conversion. In this example, the model binder first looks in a table of known locations, and if that fails, it uses type conversion.
Setting the Model Binder
There are several ways to set a model binder. First, you can add a [ModelBinder] attribute to the parameter.
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
You can also add a [ModelBinder] attribute to the type. Web API will use the specified model binder for all parameters of that type.
[ModelBinder(typeof(GeoPointModelBinder))] public class GeoPoint { // .... }
Upvotes: 0
Reputation: 4789
Assumption: You are actually doing some command with the data.
If your payload to the server is getting more complex than a simple route can handle, consider using a POST
http verb and send it to the server as JSON
instead of mangling the uri to shoehorn it in as a GET
.
Different assumption: You are doing a complex fetch and GET
is idiomatically correct for a RESTFUL service.
Use a querystring, per the answer posted by @TrevorPilley
Upvotes: 0
Reputation: 16393
You might be able to deal with it by accepting an array:
public HttpResponseMessage MyFunction(
string action,
string[] values)
Mapping the route as:
api/{controller}/{action}
And using the query string to supply values:
GET http://server/api/Controller?values=1&values=2&values=3
Upvotes: 1