在REST API中实现多个参数过滤器的最简洁方法

时间:2018-11-22 16:07:25

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

我目前正在实现RESTFUL API,该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结合使用来实现此目的。

2 个答案:

答案 0 :(得分: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;
}

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

答案 1 :(得分:0)

我已经发明并发现将一些过滤器组合为一种类型(例如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"});
        }