ASP.NET Core API search parameters from path/route


ASP.NET Core API search parameters from path/route



I am porting a PHP/CI API that uses $params = $this->uri->uri_to_assoc() so that it can accept GET requests with many combinations, such as:


$params = $this->uri->uri_to_assoc()



With lots of code like:


$page = 1;
if (!empty($params['page'])) {
$page = (int)$params['page'];
}



1) Conventional routing with catchall:


app.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Properties}/{action=Search}/{*params}"
);
});



But now I have to parse the params string for the key/value pairs and am not able to take advantage of model binding.


params



2) Attribute routing:


[HttpGet("properties/search")]
[HttpGet("properties/search/beds/{beds}")]
[HttpGet("properties/search/beds/{beds}/page/{page}")]
[HttpGet("properties/search/page/{page}/beds/{beds}")]
public IActionResult Search(int beds, double lat, double lon, int page = 1, int limit = 10) {
}



Obviously putting every combination of allowed search parameters and values is tedious.




3 Answers
3




FromPath value provider


FromPath



What you are wanting is to bind a complex model to part of the url path. Unfortunately, ASP.NET Core does not have a built-in FromPath binder. Fortunately, though, we can build our own.


FromPath



Here is an example FromPathValueProvider in GitHub that has the following result:


FromPathValueProvider



enter image description here



Basically, it is binding domain.com/controller/action/key/value/key/value/key/value. This is different than what either the FromRoute or the FromQuery value providers do.


domain.com/controller/action/key/value/key/value/key/value


FromRoute


FromQuery



Use the FromPath value provider


FromPath



Create a route like this:


routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*path}"
);



Add the [FromPath] attribute to your action:


[FromPath]


public IActionResult Search([FromPath]BedsEtCetera model)
{
return Json(model);
}



And magically it will bind the *path to a complex model:


*path


public class BedsEtCetera
{
public int Beds { get; set; }
public int Page { get; set; }
public string Sort { get; set; }
}



Create the FromPath value provider


FromPath



Create a new attribute based on FromRoute.


FromRoute


[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property,
AllowMultiple = false, Inherited = true)]
public class FromPath : Attribute, IBindingSourceMetadata, IModelNameProvider
{
/// <inheritdoc />
public BindingSource BindingSource => BindingSource.Custom;

/// <inheritdoc />
public string Name { get; set; }
}



Create a new IValueProviderFactory base on RouteValueProviderFactory.


RouteValueProviderFactory


public class PathValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
var provider = new PathValueProvider(
BindingSource.Custom,
context.ActionContext.RouteData.Values);

context.ValueProviders.Add(provider);

return Task.CompletedTask;
}
}



Create a new IValueProvider base on RouteValueProvider.


RouteValueProvider


public class PathValueProvider : IValueProvider
{
public Dictionary<string, string> _values { get; }

public PathValueProvider(BindingSource bindingSource, RouteValueDictionary values)
{
if(!values.TryGetValue("path", out var path))
{
var msg = "Route value 'path' was not present in the route.";
throw new InvalidOperationException(msg);
}

_values = (path as string).ToDictionaryFromUriPath();
}

public bool ContainsPrefix(string prefix) => _values.ContainsKey(prefix);

public ValueProviderResult GetValue(string key)
{
key = key.ToLower(); // case insensitive model binding
if(!_values.TryGetValue(key, out var value)) {
return ValueProviderResult.None;
}

return new ValueProviderResult(value);
}
}



The PathValueProvider uses a ToDictionaryFromUriPath extension method.


PathValueProvider


ToDictionaryFromUriPath


public static class StringExtensions {
public static Dictionary<string, string> ToDictionaryFromUriPath(this string path) {
var parts = path.Split('/');
var dictionary = new Dictionary<string, string>();
for(var i = 0; i < parts.Length; i++)
{
if(i % 2 != 0) continue;
var key = parts[i].ToLower(); // case insensitive model binding
var value = parts[i + 1];
dictionary.Add(key, value);
}

return dictionary;
}
}



Wire things together in your Startup class.


Startup


public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddMvcOptions(options =>
options.ValueProviderFactories.Add(new PathValueProviderFactory()));
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(routes => {
routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*path}"
);
});
}
}



Here is a working sample on GitHub.



My other answer is a better option.



$params = $this->uri->uri_to_assoc() turns a URI into an associative array, which is basically a .NET Dictionary<TKey, TValue>. We can do something similar in ASP.NET Core. Lets say we have the following routes.


$params = $this->uri->uri_to_assoc()


Dictionary<TKey, TValue>


app.UseMvc(routes => {
routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*params}"
);
});



Action


public class PropertiesController : Controller
{
public IActionResult Search(string slug)
{
var dictionary = slug.ToDictionaryFromUriPath();
return Json(dictionary);
}
}



Extension Method


public static class UrlToAssocExtensions
{
public static Dictionary<string, string> ToDictionaryFromUriPath(this string path) {
var parts = path.Split('/');
var dictionary = new Dictionary<string, string>();
for(var i = 0; i < parts.Length; i++)
{
if(i % 2 != 0) continue;
var key = parts[i];
var value = parts[i + 1];
dictionary.Add(key, value);
}

return dictionary;
}
}



The result is an associative array based on the URI path.


{
"beds": "3",
"page": "1",
"sort": "price_desc"
}



But now I have to parse the params string for the key/value pairs and am not able to take advantage of model binding.



Bind Uri Path to Model



If you want model binding for this, then we need to go a step further.



Model


public class BedsEtCetera
{
public int Beds { get; set; }
public int Page { get; set; }
public string Sort { get; set; }
}



Action


public IActionResult Search(string slug)
{
BedsEtCetera model = slug.BindFromUriPath<BedsEtCetera>();
return Json(model);
}



Additional Extension Method


public static TResult BindFromUriPath<TResult>(this string path)
{
var dictionary = path.ToDictionaryFromUriPath();
var json = JsonConvert.SerializeObject(dictionary);
return JsonConvert.DeserializeObject<TResult>(json);
}



IMHO you are looking at this from the wrong perspective.



Create a model:


public class FiltersViewModel
{
public int Page { get; set; } = 0;
public int ItemsPerPage { get; set; } = 20;
public string SearchString { get; set; }
public string Platforms { get; set; }
}



API Endpoint:


[HttpGet]
public async Task<IActionResult> GetResults([FromRoute] ViewModels.FiltersViewModel filters)
{
// process the filters here
}



Result Object (dynamic)


public class ListViewModel
{
public object items;
public int totalCount = 0;
public int filteredCount = 0;
}





FromQuery binds to the query string (after the ? in the URL) not to the path. As a result, it will not work for the /key/value/key/value/... pattern.
– Shaun Luttin
Jun 30 at 2:59



FromQuery


?


/key/value/key/value/...





My bad, typed too quickly ;)
– Dementic
yesterday






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Comments

Popular posts from this blog

paramiko-expect timeout is happening after executing the command

Export result set on Dbeaver to CSV

Opening a url is failing in Swift