Skip to content
No description, website, or topics provided.
C#
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src
test
.gitattributes
.gitignore
DataFilters.sln
Nuget.config
README.md
build.yaml
core.props
tests.props

README.md

Datafilters Build status

DataFilters is a small library that allow to convert a string to a generic IFilterobject. Highly inspired by the elastic syntax, it offers a powerful way to build and query data with a syntax that's not bound to a peculiar datasource.

  1. Introduction
  2. Filtering i. Starts with ii. Ends with iii. Contains
    iv. Greater than or equal v. Less than or equal vi. Between
  3. Sorting
  4. How to use
  5. How to install

Introduction

The idea came to me when working on a set of REST apis and trying to build /search endpoints. I wanted to have a uniform way to query a collection of resources whilst abstracting away underlying datasources.

Let's say your api manage vigilante resources :

class Vigilante
{
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public string Nickname {get; set; }
    public int Age { get; set; }
}

and the base url of your api is https://my-beautiful/api.

vigilante resources could then be located at https://my-beautiful/api/vigilantes/

Wouldn't it be nice to be able to search any resource like so https://my-beautiful/api/vigilantes/search?nickname=Bat*|Super* ?

This is exactly what this project is about : giving you an uniform syntax to query resources without having to thing about the underlying datasource.

Filtering

The main classes to deal with are :

  • IFilter is the interface that describes the shape of filters. There are two kind of filter
  • Filter is an implementation of a filter on a single property
  • CompositeFilter : combines several Filters using a logical operator that can be AND or OR.

The library supports a custom syntax that can be used to specified one or more criteria resources must fullfill. The currently supported syntax mimic the query string syntax : a key-value pair separated by ampersand (& character) where :

  • field is the name of a property of the resource to filter
  • value is an expression which syntax is highly inspired by the Lucene syntax

Several expressions are supported and here's how you can start using them in your search queries.

Starts with

Search for any vigilante resource that starts with "bat" in the nickname property

"nickname=bat*"

Search for any vigilante resource that starts with "bat" in the

will result in a IFilter instance equivalent to

IFilter filter = new Filter("nickname", StartsWith, "bat");

Ends with

Search for vigilante resource that ends with man in the nickname property.

"nickname=*man"

Contains

Search for vigilante resources that contains bat in the nickname property.

"nickname=*bat*"

Greater than or equal

Search for vigilante resources where the value of age property is greater than or equal to 18

"age=[* TO 18]"

will result in a IFilter instance equivalent to

IFilter filter = new Filter("age", GreaterThanOrEqualTo, 18);

Less than or equal

Search for vigilante resource where the value of age property is lower than 30

"age=[* TO 30]"

will be parsed into a IFilter equivalent to

IFilter filter = new Filter("age", LessThanOrEqualTo, 30);
Between

Search for vigilante resources where age property is between 20 and 35

"age=[20 TO 35]" 

will result in a IFilter instance equivalent to

IFilter filter = new CompositeFilter
{
    Logic = Or,
    Filters = new IFilter[]
    {
        new Filter("age", GreaterThanOrEqualTo, 20),
        new Filter("age", LessThanOrEqualTo, 35)
    }
}

Logical operators

Logicial operators helps combine several instances of IFilter

And

The , to combine multiple expressions

"nickname=Bat*,*man"

will result in a IFilter instance equivalent to

x => x.Nickname.StartsWith("Bat") && x.Nickname.EndsWith("man")
Or

Search for vigilante resources where the value of the nickname property either starts with "Bat" or ends with "man"

"nickname=Bat*|*man"

will result in

Ifilter filter = new CompositeFilter 
{
    Logic = Or,
    Filters = new IFilter[]
    {
        new Filter("nickname", StartsWith, "Bat"),
        new Filter("nickname", EndsWith, "man")
    }
}

Not

To invert a filter, simply put a ! before the expression to negate

Search for vigilante resources where the value of nickname property does not starts with "B"

"nickname=!B*"

will be parsed into a IFilter instance equivalent to

IFilter filter = new Filter("nickname", DoesNotStartWith, "B");

Expressions can be arbitrarily complex.

"nickname=(Bat*|Sup*)|(*man|*er)"

Explanation :

The criteria under construction will be applied to the value of nickname property and can be read as follow :

Searchs for vigilante resources that starts with Bat or Sup and ends with man or er.

will be parsed into a

IFilter filter = new CompositeFilter
{
    Logic = Or,
    Filters = new IFilter[]
    {
        new CompositeFilter
        {
            Logic = Or,
            Filters = new IFilter[]
            {
                new Filter("Firstname", StartsWith, "Bat"),
                new Filter("Firstname", StartsWith, "Sup"),
            }
        },
        new CompositeFilter
        {
            Logic = Or,
            Filters = new IFilter[]
            {
                new Filter("Firstname", EndsWith, "man"),
                new Filter("Firstname", EndsWith, "er"),
            }
        },
    }
}

The ( and ) allows to group two expressions together so that this group can be used as a more complex expression unit.

Sorting

This library also supports a custom syntax to sort elements.

sort=nickname or sort=+nickname sort items by their nickname properties in ascending order.

You can sort by several properties at once by separating them with a ,.

For example sort=+nickname,-age allows to sort by nickname ascending, then by age property descending.

How to use

So you have your API and want provide a great search experience ?

On the client

The client will have the responsability of building search criteria. Go to filtering and sorting sections to see example on how to get started.

On the backend

One way to start could be by having a dedicated resource which properties match the resource's properties search will be performed onto.

Continuing with our vigilante API, we could have

// Wraps the search criteria for Vigilante resources.
public class SearchVigilanteQuery
{
    public string Firstname {get; set;}

    public string Lastname {get; set;}

    public string Nickname {get; set;}

    public int? Age {get; set;} 
}

and the following endpoint

using DataFilters;

public class VigilantesController 
{
    // code omitted for brievity

    [HttpGet("search")]
    public ActionResult Search([FromQuery] SearchVigilanteQuery query)
    {
        IList<IFilter> filters = new List<IFilter>();

        if(!string.IsNullOrWhitespace(query.Firstname))
        {
            filters.Add($"{nameof(Vigilante.Firstname)}={query.Firstname}".ToFilter<Vigilante>());
        }

        if(!string.IsNullOrWhitespace(query.Lastname))
        {
            filters.Add($"{nameof(Vigilante.Lastname)}={query.Lastname}".ToFilter<Vigilante>());
        }

        if(!string.IsNullOrWhitespace(query.Nickname))
        {
            filters.Add($"{nameof(Vigilante.Nickname)}={query.Nickname}".ToFilter<Vigilante>());
        }

        if(query.Age.HasValue)
        {
            filters.Add($"{nameof(Vigilante.Age)}={query.Age.Value}".ToFilter<Vigilante>());
        }


        IFilter  filter = filters.Count() == 1
            ? filters.Single()
            : new CompositeFilter{ Logic = And, Filters = filters };

        // filter now contains how search criteria and is ready to be used :-) 

    }
}

Some explanation on the controller's code above (assuming ):

  1. The endpoint is bound to incoming HTTP GET requests on /vigilante/search
  2. The framework will parse incoming querystring and feeds the query parameter accordingly.
  3. From this point we test each criterion to see if it's acceptable to turn it into a IFilter instance. For that purpose, a handy .ToFilter<T>() string extension is available. It turns a query-string key-value pair into a full IFilter.
  4. we can then either :
    • use the filter directly is there was only one filter
    • or combine them using composite filter if there were more than one criteria

You may have notice that SearchVigilanteQuery.Age property is nullable whereas Vigilante.Age is not. This is to distinguuish if the Age criterion was provided or not when calling the vigilantes/search endpoint

How to install

  1. run dotnet install DataFilters : you can already start to build IFilter instances
  2. You can then add Datafilters.Expressions package to build Expression<Func<T, bool>>
You can’t perform that action at this time.