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:
我正在移植一个PHP/CI API,它使用$params = $this->uri->uri_to_assoc(),以便它能够接受具有多种组合的GET请求,例如:
- https://server/properties/search/beds/3/page/1/sort/price_desc
- https://server/properties/search/beds/3/page/1/sort/price_desc
- https://server/properties/search/page/2/lat/34.1/lon/-119.1
- https://server/properties/search/page/2/lat/34.1/lon/ - 119.1
- https://server/properties/search
- https://server/properties/search
- etc
- 等
With lots of code like:
有很多代码,比如:
$page = 1;
if (!empty($params['page'])) {
$page = (int)$params['page'];
}
The two ASP.NET Core 2.1 techniques I've tried both seem like a kludge so I would appreciate any guidance on a better solution:
1) Conventional routing with catchall:
1)常规路由:
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:
2)属性路由:
[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.
显然,将所有允许的搜索参数和值组合在一起是很繁琐的。
Changing the signature of these endpoints is not an option.
3 个解决方案
#1
1
FromPath
value provider
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.
您需要的是将复杂模型绑定到url路径的一部分。不幸的是,ASP。NET Core没有内置的FromPath绑定器。幸运的是,我们可以自己建造。
Here is an example FromPathValueProvider
in GitHub that has the following result:
下面是GitHub上的pathvalueprovider示例,其结果如下:
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.
基本上,它是binding domain.com/controller/action/key/value/key/value/key/value。这与FromRoute或FromQuery值提供者所做的不同。
Use the FromPath
value provider
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:
神奇的是,它将把*路径绑定到一个复杂的模型:
public class BedsEtCetera
{
public int Beds { get; set; }
public int Page { get; set; }
public string Sort { get; set; }
}
Create the FromPath
value provider
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上创建一个新的IValueProviderFactory基础。
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上创建一个新的IValueProvider基。
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.
将启动类中的东西连接在一起。
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.
这是GitHub上的一个工作示例。
#2
1
Edit
My other answer is a better option.
我的另一个答案是更好的选择。
General Idea
$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()将一个uri转换为一个关联数组,它基本上是一个。net字典
app.UseMvc(routes => {
routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*params}"
);
});
Bind Uri Path to Dictionary
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.
结果是一个基于URI路径的关联数组。
{
"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.
但是现在我必须解析键/值对的params字符串,不能利用模型绑定。
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);
}
#3
0
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:
API端点:
[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;
}
#1
1
FromPath
value provider
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.
您需要的是将复杂模型绑定到url路径的一部分。不幸的是,ASP。NET Core没有内置的FromPath绑定器。幸运的是,我们可以自己建造。
Here is an example FromPathValueProvider
in GitHub that has the following result:
下面是GitHub上的pathvalueprovider示例,其结果如下:
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.
基本上,它是binding domain.com/controller/action/key/value/key/value/key/value。这与FromRoute或FromQuery值提供者所做的不同。
Use the FromPath
value provider
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:
神奇的是,它将把*路径绑定到一个复杂的模型:
public class BedsEtCetera
{
public int Beds { get; set; }
public int Page { get; set; }
public string Sort { get; set; }
}
Create the FromPath
value provider
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上创建一个新的IValueProviderFactory基础。
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上创建一个新的IValueProvider基。
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.
将启动类中的东西连接在一起。
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.
这是GitHub上的一个工作示例。
#2
1
Edit
My other answer is a better option.
我的另一个答案是更好的选择。
General Idea
$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()将一个uri转换为一个关联数组,它基本上是一个。net字典
app.UseMvc(routes => {
routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*params}"
);
});
Bind Uri Path to Dictionary
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.
结果是一个基于URI路径的关联数组。
{
"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.
但是现在我必须解析键/值对的params字符串,不能利用模型绑定。
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);
}
#3
0
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:
API端点:
[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;
}