【问题标题】:Cleanest way to implement multiple parameters filters in a REST API在 REST API 中实现多参数过滤器的最简洁方法
【发布时间】:2020-06-24 20:23:52
【问题描述】:

我目前正在实现一个 RESTFUL API,它提供与数据库接口的端点。

我想在我的 API 中实现过滤,但我需要提供一个端点,该端点可以提供一种使用表的所有列对表应用过滤的方法。

我发现了一些模式,例如:

GET /api/ressource?param1=value1,param2=value2...paramN=valueN

param1,param2...param N 是我的表列和值。

我还发现了另一种模式,它包含发送一个表示查询的 JSON 对象。

要过滤一个字段,只需将该字段及其值添加到查询中:

GET /app/items
{
  "items": [
    {
      "param1": "value1",
      "param2": "value",
      "param N": "value N"
    }
  ]
}

我正在寻找实现这一目标的最佳实践。

我正在使用 EF Core 和 ASP.NET Core 来实现这一点。

【问题讨论】:

    标签: entity-framework rest asp.net-web-api


    【解决方案1】:

    首先要谨慎过滤所有内容/任何内容。基于用户需要的可用过滤器,并根据需求进行扩展。需要编写的代码更少、复杂性更低、数据库端所需的索引更少、性能更好。

    也就是说,对于具有大量过滤器的页面,我使用的方法是使用枚举服务器端,在该服务器端,我的条件字段被传回其枚举值(数字)以在请求中提供。因此,过滤器字段将包含名称、默认值或适用值,以及在将输入或选择的值传递回搜索时使用的枚举值。请求代码创建一个 JSON 对象,其中包含应用的过滤器和 Base64 以在请求中发送:

    {
      p1: "Jake",
      p2: "8"
    }
    

    查询字符串如下所示: .../api/customer/search?filters=XHgde0023GRw....

    在服务器端,我提取 Base64,然后将其解析为 Dictionary<string,string> 以供过滤器解析。例如,假设条件是使用姓名和年龄搜索孩子:

    // this is the search filter keys, these (int) values are passed to the search client for each filter field.
    public enum FilterKeys
    {
        None = 0,
        Name,
        Age,
        ParentName
    }
    
    public JsonResult Search(string filters)
    {
        string filterJson = Encoding.UTF8.GetString(Convert.FromBase64String(filters));
        var filterData = JsonConvert.DeserializeObject<Dictionary<string, string>>(filterJson);
    
        using (var context = new TestDbContext())
        {
            var query = context.Children.AsQueryable();
    
            foreach (var filter in filterData)
                query = filterChildren(query, filter.Key, filter.Value);
    
            var results = query.ToList(); //example fetch.
            // TODO: Get the results, package up view models, and return...
        }
    }
    
    private IQueryable<Child> filterChildren(IQueryable<Child> query, string key, string value)
    {
        var filterKey = parseFilterKey(key);
        if (filterKey == FilterKeys.None)
            return query;
    
        switch (filterKey)
        {
            case FilterKeys.Name:
                query = query.Where(x => x.Name == value);
                break;
            case FilterKeys.Age:
                DateTime birthDateStart = DateTime.Today.AddYears((int.Parse(value) + 1) * -1);
                DateTime birthDateEnd = birthDateStart.AddYears(1);
                query = query.Where(x => x.BirthDate <= birthDateEnd && x.BirthDate >= birthDateStart);
                break;
        }
        return query;
    }
    
    private FilterKeys parseFilterKey(string key)
    {
        FilterKeys filterKey = FilterKeys.None;
    
        Enum.TryParse(key.Substring(1), out filterKey);
        return filterKey;
    }
    

    您可以使用字符串和常量来避免枚举解析,但是我发现枚举是可读的并且使发送的有效负载更加紧凑。以上是一个简化的示例,显然需要进行错误检查。复杂过滤条件的实现代码(例如上面的年龄到出生日期)更适合作为单独的方法,但它应该会给您一些想法。例如,您可以按姓名、和/或年龄和/或父母姓名搜索孩子。

    【讨论】:

      【解决方案2】:

      我发明并发现将一些过滤器组合成一种类型(例如 CommonFilters)很有用,并使这种类型可从字符串中解析:

      [TypeConverter(typeof(CommonFiltersTypeConverter))]
      public class CommonFilters
      {
          public PageOptions PageOptions { get; set; }
      
          public Range<decimal> Amount { get; set; }
            
          //... other filters 
      
          [JsonIgnore]
          public bool HasAny => Amount.HasValue || PageOptions!=null;
      
          public static bool TryParse(string str, out CommonFilters result)
          {
              result = new CommonFilters();
              if (string.IsNullOrEmpty(str))
                  return false;
      
              var parts = str.Split(new[] { ' ', ';' }, StringSplitOptions.RemoveEmptyEntries);
              foreach (var part in parts)
              {
                  if (part.StartsWith("amount:") && Range<decimal>.TryParse(part.Substring(7), out Range<decimal> amount))
                  {
                      result.Amount = amount;
                      continue;
                  }
                  if (part.StartsWith("page-options:") && PageOptions.TryParse(part.Substring(13), out PageOptions pageOptions))
                  {
                      result.PageOptions = pageOptions;
                      continue;
                  }
                  //etc.
              }
              return result.HasAny;
          }
      
          public static implicit operator CommonFilters(string str)
          {
              if (TryParse(str, out CommonFilters res))
                  return res;
              return null;
          }
      
      }
      
      
      public class CommonFiltersTypeConverter : TypeConverter
      {
      
          public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
          {
              if (sourceType == typeof(string))
              {
                  return true;
              }
      
              return base.CanConvertFrom(context, sourceType);
          }
      
          public override object ConvertFrom(ITypeDescriptorContext context,
              CultureInfo culture, object value)
          {
              if (value is string str)
              {
                  if (CommonFilters.TryParse(str, out CommonFilters obj))
                  {
                      return obj;
                  }
              }
      
              return base.ConvertFrom(context, culture, value);
          }
      
      }
      

      请求如下所示:

      public class GetOrdersRequest
          {
              [DefaultValue("page-options:50;amount:0.001-1000;min-qty:10")]
              public CommonFilters Filters { get; set; }
              
              //...other stuff
          }
      

      这样可以减少输入请求参数的数量,尤其是当某些查询不关心所有过滤器时

      如果你使用 swagger map 这个类型作为字符串:

      c.MapTypeAsString<CommonFilters>();
      
      public static void MapTypeAsString<T>(this SwaggerGenOptions swaggerGenOptions)
              {
                  swaggerGenOptions.MapType(typeof(T), () => new OpenApiSchema(){Type = "string"});
              }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-12-17
        • 2017-05-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-01-28
        • 2022-01-26
        • 1970-01-01
        相关资源
        最近更新 更多