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
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
?
/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.
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