来自路径/路由的ASP.NET核心API搜索参数

来自路径/路由的ASP.NET核心API搜索参数,asp.net,rest,asp.net-core,Asp.net,Rest,Asp.net Core,我正在移植一个使用$params=$this->uri->uri_to_assoc()的PHP/API,以便它可以接受多种组合的GET请求,例如: 等 有很多代码,比如: $page = 1; if (!empty($params['page'])) { $page = (int)$params['page']; } 我尝试过的两种ASP.NET Core 2.1技术看起来都是一个难题,因此我希望您能为我提供更好的解决方案: 1) 使用catchall的常规路由: app.U

我正在移植一个使用
$params=$this->uri->uri_to_assoc()
的PHP/API,以便它可以接受多种组合的GET请求,例如:

有很多代码,比如:

$page = 1;
if (!empty($params['page'])) {
    $page = (int)$params['page'];
}
我尝试过的两种ASP.NET Core 2.1技术看起来都是一个难题,因此我希望您能为我提供更好的解决方案: 1) 使用catchall的常规路由:

app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Properties}/{action=Search}/{*params}"
                );
            });
但是现在我必须解析键/值对的
params
字符串,不能利用模型绑定

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) {
}
显然,将允许的搜索参数和值的每一个组合放在一起都是乏味的

更改这些终结点的签名不是一个选项。编辑 这是一个更好的选择

总体思路
$params=$this->uri->uri\u to\u assoc()
将uri转换为关联数组,基本上是一个.NET
字典。我们可以在ASP.NET内核中执行类似的操作。假设我们有以下路线

app.UseMvc(routes => {
    routes.MapRoute(
        name: "properties-search",
        template: "{controller=Properties}/{action=Search}/{*params}"
    );
});
将Uri路径绑定到字典 行动

public class PropertiesController : Controller
{
    public IActionResult Search(string slug)
    {
        var dictionary = slug.ToDictionaryFromUriPath();
         return Json(dictionary);
    }
}
public IActionResult Search(string slug)
{
    BedsEtCetera model = slug.BindFromUriPath<BedsEtCetera>();
    return Json(model);
}
扩展方法

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;
    }
}
但是现在我必须解析键/值对的params字符串,不能利用模型绑定

将Uri路径绑定到模型 如果您希望为此进行模型绑定,那么我们需要更进一步

模型

行动

public class PropertiesController : Controller
{
    public IActionResult Search(string slug)
    {
        var dictionary = slug.ToDictionaryFromUriPath();
         return Json(dictionary);
    }
}
public IActionResult Search(string slug)
{
    BedsEtCetera model = slug.BindFromUriPath<BedsEtCetera>();
    return Json(model);
}
public IActionResult搜索(字符串段塞)
{
床头柜模型=slug.BindFromUriPath();
返回Json(模型);
}
附加扩展方法

public static TResult BindFromUriPath<TResult>(this string path)
{
    var dictionary = path.ToDictionaryFromUriPath();
    var json = JsonConvert.SerializeObject(dictionary);
    return JsonConvert.DeserializeObject<TResult>(json);
}
公共静态TResult BindFromUriPath(此字符串路径)
{
var dictionary=path.ToDictionaryFromUriPath();
var json=JsonConvert.SerializeObject(字典);
返回JsonConvert.DeserializeObject(json);
}

我想你是从错误的角度看待这个问题的

创建模型:

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端点:

[HttpGet]
public async Task<IActionResult> GetResults([FromRoute] ViewModels.FiltersViewModel filters)
{
    // process the filters here
}
FromPath
值提供程序 您想要的是将复杂模型绑定到url路径的一部分。不幸的是,ASP.NET核心没有内置的
FromPath
binder。幸运的是,我们可以建立自己的

下面是一个具有以下结果的示例:

基本上,它是绑定
domain.com/controller/action/key/value/key/value/key/value
。这与
FromRoute
FromQuery
值提供程序所做的不同

使用
FromPath
值提供程序 创建如下所示的路线:

routes.MapRoute(
    name: "properties-search",
    template: "{controller=Properties}/{action=Search}/{*path}"
);
[FromPath]
属性添加到操作中:

public IActionResult Search([FromPath]BedsEtCetera model)
{
    return Json(model);
}
它神奇地将
*路径
绑定到一个复杂的模型:

public class BedsEtCetera 
{
    public int Beds { get; set; }
    public int Page { get; set; }
    public string Sort { get; set; }
}
创建
FromPath
值提供程序 基于创建新属性

基于创建新的IValueProvider

在您的
启动
类中将所有内容连接在一起

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}"
            );
        });
    }
}

这是

FromQuery
绑定到查询字符串(在URL中的
之后),而不是路径。因此,它不适用于
/key/value/key/value/…
模式
[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; }
}
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;
    }
}
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);
    }
}
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;
    }
}
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}"
            );
        });
    }
}