【问题标题】:ASP.NET Core API search parameters from path/route来自路径/路由的 ASP.NET Core API 搜索参数
【发布时间】:2018-07-02 01:22:27
【问题描述】:

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

有很多类似的代码:

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

我尝试过的两种 ASP.NET Core 2.1 技术看起来都很杂乱,所以如果有任何关于更好解决方案的指导,我将不胜感激:

1) 包罗万象的常规路由:

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) {
}

显然,将允许的搜索参数和值的每一种组合都放在一起是很乏味的。

更改这些端点的签名不是一种选择。

【问题讨论】:

    标签: asp.net rest asp.net-core


    【解决方案1】:

    FromPath价值提供者

    您想要的是将复杂模型绑定到 url 路径的一部分。不幸的是,ASP.NET Core 没有内置的FromPath binder。不过幸运的是,我们可以自己构建。

    这是一个example FromPathValueProvider in GitHub,其结果如下:

    基本上,它绑定domain.com/controller/action/key/value/key/value/key/value。这与FromRouteFromQuery 值提供者所做的不同。

    使用FromPath 值提供者

    创建这样的路线:

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

    [FromPath] 属性添加到您的操作中:

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

    它会神奇地将*path 绑定到一个复杂的模型:

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

    创建FromPath 值提供者

    基于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; }
    }
    

    基于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;
        }
    }
    

    基于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);
        }
    }
    

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

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

    这里是a working sample on GitHub

    【讨论】:

      【解决方案2】:

      编辑

      My other answer 是更好的选择。

      总体思路

      $params = $this-&gt;uri-&gt;uri_to_assoc() 将 URI 转换为关联数组,这基本上是一个 .NET Dictionary&lt;TKey, TValue&gt;。我们可以在 ASP.NET Core 中做类似的事情。假设我们有以下路线。

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

      结果是一个基于 URI 路径的关联数组。

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

      但现在我必须解析键/值对的参数字符串,并且无法利用模型绑定。

      将 Uri 路径绑定到模型

      如果您想要为此进行模型绑定,那么我们需要更进一步。

      型号

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

      动作

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

      其他扩展方法

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

      【讨论】:

        【解决方案3】:

        恕我直言,您从错误的角度看待这个问题。

        创建模型:

        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
        }
        

        结果对象(动态)

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

        【讨论】:

        • FromQuery 绑定到查询字符串(在 URL 中的 ? 之后)而不是路径。因此,它不适用于/key/value/key/value/... 模式。
        • 我的错,打字太快了;)
        猜你喜欢
        • 2021-04-16
        • 2019-09-29
        • 1970-01-01
        • 2018-07-20
        • 2018-08-28
        • 1970-01-01
        • 2017-04-13
        • 2019-09-07
        • 2021-06-17
        相关资源
        最近更新 更多