1

This is a bit of a more complex scenario than binding a comma delimited query parameter string to an array of those comma tokenized values.

Basically I have a simple entity model called User:

public class User
{
    public string Name { get; set }
    public int Age { get; set; }
    // rest ommited
}

I'd like to be able to provide multiple names and ages in my web api endpoint like such:

http://localhost/api/data/user/query?name=chris,john,alex&age=28,35

which would bind to an ARRAY of user filters (i'm just using the entity model). And also Swagger would show the test input form as if it was a single User fields you are entering in.

How can this be done with AspDotNetCore 2.0 Web Api and Swashbucke without having to create a new model with strings and tokenize on comma. I'd love to create some kind of Swashbuckle/AspNetCore hook (decorate method parameter with some attribute) that would then know to split inputs into seperate models.

The idea being I generate the filter Expression> for filtering data from a repository using the collection of filter values:

var exampleExpression = entity => (entity.Name.Contains("chris") || entity.Name.Contains("john") || entity.Name.Contains("alex")) && (entity.Age == 28 || entity.Age == 35);

Generating the Expression tree programatically I can do.

I'm even playing with the idea of taking the inputs and putting them into a dictionary for lookup when generating the filter expression. But I'd love to hear your thoughts and solution.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
karczilla
  • 105
  • 2
  • 12
  • You should consider using a POST, you will run into problems with the maximum length of the URL. With lots of parameters a POST is acceptable, we do it all the time. – Helder Sepulveda Oct 20 '17 at 15:22

1 Answers1

5

The only way I'm aware of doing this is to write a custom implementation of IModelBinder. I'm doing something very similar here.

CsvModelBinder.cs

public class CsvModelBinder<T> : IModelBinder where T : IConvertible
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var fieldName = bindingContext.FieldName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);

        var model = new List<T>();

        foreach(string delimitedString in valueProviderResult.Values)
        {
            var splitValues = delimitedString
                .Split(',')
                .Cast<string>();

            var convertedValues = splitValues
                .Select(str => Convert.ChangeType(str, typeof(T)))
                .Cast<T>();

            model.AddRange(convertedValues);
        }

        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

ModelsController.cs

[Route("models")]
public class ModelsController : Controller
{
    [HttpGet]
    [Route("{ids}")]
    [Produces(typeof(IEnumerable<Model>))]
    public IActionResult Get
    (
        [ModelBinder(typeof(CsvModelBinder<string>))] IEnumerable<string> ids
    )
    {
        // Get models

        return Ok(models);
    }
}

Hope this helps! Also, if you have any insight to offer on my related problem, that would be much appreciated.

Raymond Saltrelli
  • 4,071
  • 2
  • 33
  • 52