I'm building a RESTful API in ASP.NET Core (3.0). Each controller has a method used to search domain objects which accepts a List<KeyValuePair<string, string>> as a querystring parameter defined like this:
[HttpGet]
[ProducesResponseType(200)]
public ActionResult<IEnumerable<Service.Models.Asset>> Get(
[FromQuery(Name = "criteria")] List<KeyValuePair<string, string>> criteria,
[FromQuery] int? page = 0,
[FromQuery] int? size = null)
{
var dtoList = _assetService.SearchPaged(criteria, page, size);
return Ok(dtoList);
}
I'm passing the criteria as a JSON serialised string, the request URI is:
http://api/asset?criteria=[{"key":"uprn","value":"h1"},{"key":"uprn","value":"h2"}]&page=0&size=100
I can't get the controller to accept a value for the criteria parameter, the criteria list is always zero-length. I've tried URL encoding the text, makes no difference.
If I change the parameter to a string, I can deserialize the parameter using JsonConvert successfully (same URI).
[HttpGet]
[ProducesResponseType(200)]
public ActionResult<IEnumerable<Service.Models.Asset>> Get(
[FromQuery(Name = "criteria")] string criteriaString,
[FromQuery] int? page = 0,
[FromQuery] int? size = null)
{
var criteria = Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<KeyValuePair<string, string>>>(criteriaString);
var dtoList = _assetService.SearchPaged(criteria, page, size);
return Ok(dtoList);
}
I don't want to use model binding with a strongly typed parameter object because I have a generic parameter converter that builds an expression from the criteria list to apply to my EF DbSet. This is applied across a number of different controllers.
Is this possible, or should I give up and stick with my string parameter and manage the de-serialization myself?
Based on this answer on SO How to bind Json Query string in asp.net core web api you can not bind query params in json format. You need to write your own model binder for deserializing to List<KeyValuePair<string, string>>
.
Without having done tests, I guess, based on the linked answer, something like this should do the job:
Create the model binder implementation:
public class KeyValueListModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var key = bindingContext.ModelName;
var jsonString = bindingContext.ValueProvider.GetValue(key).FirstValue;
MyCustomModel result = JsonConvert.DeserializeObject<IEnumerable<KeyValuePair<string, string>>>(jsonString);
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
Create the provider:
public class KeyValueListModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(List<KeyValuePair<string, string>>))
return new KeyValueListModelBinder();
return null;
}
}
Register the provider in startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config => config.ModelBinderProviders.Insert(0, new KeyValueListModelBinderProvider()));
}