4

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

4 Answers 4

1

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
Sign up to request clarification or add additional context in comments.

Comments

0

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

4 Comments

Yeah I know, but POST is not an option. :(
I wouldn't recommend POST if you actually just want to GET something, that's part of the point of Web API is that the Verb has significant meaning.
@TrevorPilley It sounds a lot more like a command than a simple get-a-thing to me. The question is asking about taking a complex set of parameters and performing MyFunction on that list. I suppose I should have made that assumption explicitly clear.
@JonathanAnctil..you can use message body to send something with a GET request, not the recommended way but you can use that if your query string is getting complex and you cannot do a POST.
0

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 {
// .... }

1 Comment

Ho interesting! I will try your solution :)
0

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!

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.