Bind /action/1,2,3 to List

2019-05-19 06:49发布

问题:

In my API, I'd like to have routes like GET /api/v1/widgets/1,2,3 and GET /api/v1/widgets/best-widget,major-widget,bob-the-widget

public class WidgetsController : MyApiController
{
    public ActionResult Show(IEnumerable<int> ids)
    {

    }

    public ActionResult Show(IEnumerable<string> names)
    {

    }
}

I've got routes set up to get me to the action, but I can't figure out how to turn 1,2,3 into new List<int>(){1, 2, 3} and so on. Of course, I could just take a string and parse it in my action, but I'd like to avoid going that route.

One thing that came to mind was to put something in the OnActionExecuting method, but then I wasn't sure exactly what to put in there (I could hack something together, obviously, but I'm trying to write something reusable.)

The main questions I have are how to know whether I need to do anything at all (sometimes the ValueProviders upstream will have figured everything out), and how to handle figuring out the type to cast to (e.g., how do I know that in this case I need to go to a collection of ints, or a collection of strings, and then how do I do that?)

By the way, I had the idea of implementing a ValueProvider as well, but got lost on similar questions.

回答1:

I can't figure out how to turn 1,2,3 into new List(){1, 2, 3} and so on.

To avoid polluting each controller action that needs to receive this parameter I would recommend a custom model binder:

public class IdsModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var result = base.BindModel(controllerContext, bindingContext);
        var ids = bindingContext.ValueProvider.GetValue("ids");
        if (ids != null)
        {
            return ids.AttemptedValue
                      .Split(',')
                      .Select(id => int.Parse(id))
                      .ToList();
        }
        return result;
    }
}

and then register the model binder in Application_Start:

ModelBinders.Binders.Add(typeof(IEnumerable<int>), new IdsModelBinder());

and finally your controller action might look like this (and from what I can see in your question it already does look like this :-)):

public ActionResult Show(IEnumerable<int> ids)
{
    ...
}

and the custom model binder will take care for parsing the ids route token to the corresponding IEnumerable<int> value.

You could do the same with the IEnumerable<string> where you would simply remove the .Select clause in the corresponding model binder.



回答2:

if your URL was

/api/v1/widgets/Show?names=best-widget&names=major-widget&names=bob-the-widget

This would bind neatly by itself :)

No need to override modelbinders in this case. The querystring-variable names will bind to your Show-method_

public ActionResult Show(IEnumerable<string> names)

Hope this helps!



回答3:

I'm relatively new to ASP.Net MVC and so I'm not sure if there is an easier way of doing this or not, however my approach would be to do something like the following:

public class WidgetsController : MyApiController
{
    public ActionResult Show(string ids)
    {
        List<int> parsedIds = new List<int>();
        foreach (var id in ids.Split(','))
        {
            parsedIds.Add(int.Parse(id));
        }
        return Show(parsedIds);
    }

    private ActionResult Show(List<int> ids);
}

You might also want to add some more sophisticated error handling for cases where the IDs entered can't be parsed, but thats the general approach I would use.