-
-
Notifications
You must be signed in to change notification settings - Fork 198
Query Filters
YesSql.Filters.Query provides support for a dynamic search query syntax, enabling user input to safely drive a YesSql Query.
It consists of
- a configurable search parser, translating input to a universal search syntax,
- a query executor, to execute a set of search terms against a YesSql Query.
The search syntax consists of preconfigured search terms, and supports a syntax of
termname
-
seperator
of:
value
e.g. sort:created
or, in the case of a default term
value
e.g. lake
A term supports either single conditions, or multiple conditions.
A OneCondition
condition is used to describe a term with a single set of predefined values that can filter the query.
Examples of this would include sort terms, or status terms.
e.g. status:published
A ManyCondition
condition is used to describe a term which can support multiple Boolean operations against the filter.
When using a ManyCondition
term the following Boolean operations are supported
-
OR
or||
(also the default when no operation is specified) -
AND
or&&
-
NOT
or!
-
()
for grouping
Examples of this would include a title filter.
title:lake
-
title:swimming lake
->title:swimming or lake
title:"swimming lake"
title:swimming AND lake
title:(swimming AND lake) NOT beach
Filters are configured per Document
type with a builder pattern.
- Register an
IQueryParser<T>
where T is aDocument
type that will be filter against. - Add terms to the builder.
- Build
Two term types are available.
NamedTerm
DefaultTerm
WithNamedTerm
is the most common term used, and it registers a term name, and a condition that will be applied when that term name is specified.
e.g. sort:oldest
WithDefaultTerm
can only be used once in a Query Filter, and while it also specifies a term name, in this case the term name is optional, and it will be applied even when the term name is not specified.
e.g. lake
or title:lake
Each specified Term
must supply the conditions that will be applied to that term.
The following example configures an IQueryParser<BlogPost>
This example is available in the YesSql.Samples.Web
project.
services.AddSingleton<IQueryParser<BlogPost>>(sp =>
new QueryEngineBuilder<BlogPost>()
.WithNamedTerm("sort", b => b
.OneCondition((val, query) =>
{
if (Enum.TryParse<BlogPostSort>(val, true, out var e))
{
switch (e)
{
case BlogPostSort.Newest:
query.With<BlogPostIndex>().OrderByDescending(x => x.PublishedUtc);
break;
case BlogPostSort.Oldest:
query.With<BlogPostIndex>().OrderBy(x => x.PublishedUtc);
break;
default:
query.With<BlogPostIndex>().OrderByDescending(x => x.PublishedUtc);
break;
}
}
else
{
query.With<BlogPostIndex>().OrderByDescending(x => x.PublishedUtc);
}
return query;
})
.AlwaysRun()
)
.WithDefaultTerm("title", b => b
.ManyCondition(
((val, query) => query.With<BlogPostIndex>(x => x.Title.Contains(val))),
((val, query) => query.With<BlogPostIndex>(x => x.Title.IsNotIn<BlogPostIndex>(s => s.Title, w => w.Title.Contains(val))))
)
)
.Build()
);
We add two Term Conditions
- A
OneCondition
sort
term for sorting the query. - A
ManyCondition
default term and a default term for filtering against thetitle
of aBlogPost
.
The sort
term is added using WithNamedTerm
, meaning that the name sort
must be provided to activate this filter.
It has OneCondition
where the values will be parsed using the syntax sort:value
.
No Boolean operations are supported on this term.
The OneCondition
method takes a MatchQuery
method, either synchronous or asynchronous, which is applied when the filter matches.
It also specifies .AlwaysRun()
which is used to provide a default sort, when the user has not supplied any filter criteria.
The title
term is added using WithDefaultTerm
, in this case the user may supply no value, e.g. lake
or the named term title:lake
It has ManyCondition
where the value will be parse using a Boolean criteria.
It takes both a MatchQuery
and a NotMatchQuery
.
The MatchQuery
is applied when using an OR
or AND
expression and the NotMatchQuery
is applied when using a NOT
expression.
Two mapping methods are available for the OneCondition
term.
This allows the filter engine to map from a model.
It can be used when a form post is used by the UI to generate filter expressions.
For example the sort
term maybe provided via a SelectList
which posts a form.
A MapFrom
method will then map the result of the form binding to the FilterResult
enabling the controller to correctly generate the search syntax.
Example
.MapFrom<Filter>((model) =>
{
if (model.SelectedStatus != BlogPostStatus.Default)
{
return (true, model.SelectedStatus.ToString());
}
return (false, String.Empty);
})
In this mapping when the BlogPostStatus
is not the default value, the filter value will be mapped to the FilterResult
when MapFrom
is called on the FilterResult
.
e.g. filter.FilterResult.MapFrom(filter);
where filter is the CLR
type that has been bound from a form post.
The resulting filter expression can be generated with a call to filter.FilterResult.ToString()
This allows a filter result to MapTo
a model.
It can be used when a user has inputted their own search expression to map this expression to a model.
Example
.MapTo<Filter>((val, model) =>
{
if (Enum.TryParse<BlogPostStatus>(val, true, out var e))
{
model.SelectedStatus = e;
}
})
For example when the user has inputted a sort:oldest
expression applying the mapping to a model allows the model to know the value of the search expression, so it is able to display the selected sort value.
e.g. filterResult.MapTo(filter);
where filter is the CLR
type the view will bind to.
On a OneCondition
AlwaysRun
can be specified to enforce that even when the term has not been specified it's MatchQuery
will AlwaysRun
. This can be used to provide a default value to a query.
For the sort
term we check whether a sort
matches the BlogPostSort
enum, and if not, i.e. if no value has been provided we are still able to apply a default sort.
Example:
if (Enum.TryParse<BlogPostSort>(val, true, out var e))
{
...
else
{
// Apply a default sort expression.
query.With<BlogPostIndex>().OrderByDescending(x => x.PublishedUtc);
}
Once a FilterResult
has been parsed it can be executed against a query.
Example
var query = session.Query<BlogPost>();
await filterResult.ExecuteAsync(new WebQueryExecutionContext<BlogPost>(HttpContext.RequestServices, query));
posts = await query.ListAsync();
The examples used for this tutorial can be found in the YesSql.Samples.Web
project, along with examples of custom model binders which can be used to model bind a Query Engine to a Query String, or form post.