DynamicQueryBuilder
Table of Contents
- 1. What?
- 2. Why?
- 3. Installation
- 4. DynamicQueryOptions
- 5. Filters
- 6. Sorting
- 7. Accessing Nested Objects
- 8. Pagination
- 9. Web Development with DQB
1 What?
DynamicQueryBuilder(DQB) is a lightweight LINQ builder library that works dynamically with the given collection generic type.
2 Why?
The motivation behind DQB was to reverse the development cost of operations such as Filtering, Sorting, Paginating data from backend to clients. This allows client development to be more free and less time consuming for non-fullstack development workspaces.
3 Installation
You can get DQB from NuGet services with the command below or NuGet UI in visual studio.
DQB currently runs with .netstandard2.1
Install-Package DynamicQueryBuilder
4 DynamicQueryOptions
This is the class that holds everything from filters to sorts and pagination
options. DQB's has a function called ApplyFilters
which converts
this DynamicQueryOptions
class into a LINQ expression. You can check out
this object here.
4.1 Basic usage of DQB like below:
IQueryable<MyObject> myCollection = GetMyCollectionOfData(); var myOpts = new DynamicQueryOptions { /* Filters, SortingOptions, Pagination */ }; IQueryable<MyObject> dqbResults = myCollection.ApplyFilters(myOpts); return dqbResults.ToList();
5 Filters
Filters are the objects that hold your logical filters. You can see the object structure here.
5.0.1 Filter Value Conversion
Since DQB always boxes your data into an object
the actual type conversion is
being handled by DQB while transforming your filters into a LINQ expression. DQB
also can handle null
values as well.
5.0.2 Supported Filters
In, Equals, LessThan, Contains, NotEqual, EndsWith, StartsWith, GreaterThan, LessThanOrEqual, GreaterThanOrEqual, Any, All
5.0.3 Supported Logical Operators
All logical operators are supported from Conditional to Bitwise. If you do not
define a logical operator to a Filter
, DQB will choose AndAlso
as default
Logical Operator.
5.0.4 Filter Examples
An example usage of Filter
class with a flat object:
var dqbOpts = new DynamicQueryOptions { Filters = new List<Filter>() { new Filter { Value = "bar", PropertyName = "foo", Operator = FilterOperation.Equals } } }; // LINQ Translation: myCollection.Where(x => x.foo == "bar");
An example usage of Filter
class with a collection property:
var dqbOpts = new DynamicQueryOptions { Filters = new List<Filter> { new Filter { Value = new DynamicQueryOptions { Value = "some_value", Operator = FilterOperation.Equals, PropertyName = "bar" }, Operator = FilterOperation.Any, PropertyName = "foo" } } }; // LINQ Translation: myCollection.Where(x => x.foo.Any(y => y.bar == "some_value"));
An example usage of Filter
class with a logical operator, combining two filters:
var dqbOpts = new DynamicQueryOptions { Filters = new List<Filter> { new Filter { Value = "123", PropertyName = "Fizz", Operator = FilterOperation.Equals, LogicalOperator = LogicalOperator.OrElse, }, new Filter { Value = "321", PropertyName = "Fizz", Operator = FilterOperation.Equals, } } }; // LINQ Translation: myCollection.Where(x => x.Fizz == "123" || x.Fizz == "321");
6 Sorting
Sorting is extremely easy with DQB. DQB currently does not support for custom
sorting callbacks and uses default .NET's OrderBy
, OrderByDescending
,
ThenBy
and ThenByDescending
functions. Sorting should be provided via
SortOption
class which you can check out here.
6.0.1 Sorting Examples
var dqbOpts = new DynamicQueryOptions { SortOptions = new List<SortOption>() { new SortOption { SortingDirection = SortingDirection.Asc, PropertyName = "Foo" }; new SortOption { SortingDirection = SortingDirection.Desc, PropertyName = "Bar" }; } }; // LINQ Translation: myCollection.OrderBy(x => x.Foo).ThenByDescending(x => x.Bar);
7 Accessing Nested Objects
DQB can access nested object with .
delimeter like C# LINQ.
public class MyNestedClass { public int Age { get; set; } } public class MyClassToFilter { public MyNestedClass MyNestedProperty { get; set; } }
With the object structures above, we could utilize Filter
and Sort
operations like below:
Filter
new Filter { Value = "27", Operator = FilterOperation.Equals, PropertyName = "MyNestedProperty.Age" }; // LINQ Translation: myCollection.Where(x => x.MyNestedProperty.Age == 28);
Sort
new SortOption { SortingDirection = SortingDirection.Asc, PropertyName = "MyNestedProperty.Age" } // LINQ Translation: myCollection.OrderBy(x => x.MyNestedProperty.Age);
8 Pagination
Pagination can be done by specifiynig options into the PaginationOptions
member of DynamicQueryOptions
class. You can check it out here.
Pagination utilizes LINQ's Skip
and Take
functions.
8.1 Pagination Examples:
var paginationOption = new PaginationOption { Count = 10, Offset = 0, AssignDataSetCount = true }; // LINQ Translation: myCollection.Skip(0).Take(10);
8.1.1 How to access the filtered count of the query
if its required to access the total query result amount(whole set) you can access it via
int totalDataSetCount = paginationOption.DataSetCount;
9 Web Development with DQB
Web development is actually where DQB shines the most. DQB comes with an
ActionFilter
that can parse HTTP queries into DynamicQueryOptions
class.
9.1 Setting up DynamicQueryBuilderSettings
This is a singleton object that can hold static configurations for DQB like operation shortcodes, query resolution methods and data source case sensitivity. You can check out this object here.
It is usually best to create an instance of this class in your Web Projects
Startup.cs
and inject it as a singleton like below
public class Startup { public Startup(ILogger<Startup> logger, IConfiguration configuration) { this.Logger = logger; this.Configuration = configuration; } public ILogger<Startup> Logger { get; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // .. other stuff var dqbSettings = new DynamicQueryBuilderSettings { // .. your settings(explained below) }; services.AddSingleton(dqbSettings); // .. other stuff } }
9.2 Query Delivery Methods
DQB can retrieve your encoded/non-encoded queries via options like below:
- Request QueryString
Below, there is an example of configuring DQB to retrieve queries from query string
string parameterToResolveFrom = "myparamtoresolve"; Func<string, string> decodeFunction = (encodedQuery) => magicDecode(encodedQuery); new DynamicQueryBuilderSettings { // Other configurations QueryOptionsResolver = new QueryStringResolver(parameterToResolveFrom, decodeFunction) }
Tip: you can leave parameterToResolveFrom
null to resolve your queries
directly from the raw querystring.
Request HTTP Header
Below, there is an example of configuring DQB to retrieve queries from HTTP Headers
string httpHeaderName = "myhttpheadername"; Func<string, string> decodeFunction = (encodedQuery) => magicDecode(encodedQuery); new DynamicQueryBuilderSettings { // Other configurations QueryOptionsResolver = new HttpHeaderResolver(httpHeaderName, decodeFunction) }
Tip: you can always leave decodeFunction
null if your queries are not encoded.
9.3 HTTP Parameters
9.3.1 o
Parameter
- Refers to
FilterOperation
andLogicalOperator
properties ofFilter
class. - Parameter formation should be
FilterOperation|LogicalOperator
using|
as a delimeter between properties. - This parameter can be placed anywhere in the querystring.
- This parameter should be forming a triplet with
p
andv
parameters.
9.3.2 p
Parameter
- Refers to the
PropertyName
property ofFilter
class. - This parameter should be placed after the
o
parameter.. - This parameter should be forming a triplet with
o
andv
parameters.
9.3.3 v
Parameter
- Refers to the
PropertyValue
property ofFilter
class. - This parameter should be placed after the
p
parameter.. - This parameter should be forming a triplet with
o
andp
parameters.
9.3.4 s
Parameter
- Refers to the
SortOption
class. - This parameter can be placed anywhere in the querystring.
- If this parameter occurs more than once, sorting will be done in the given order.
9.3.5 offset
Parameter
- Refers to the
Offset
property ofPaginationOption
class. - This parameter can be placed anywhere in the querystring.
- If this parameter occurs more than once, the first occurence will be assigned.
9.3.6 count
Parameter
- Refers to the
Count
property ofPaginationOption
class. - This parameter can be placed anywhere in the querystring.
- If this parameter occurs more than once, the first occurence will be assigned.
9.4 HTTP Query Examples
- Valid Example: ?o=Equals&p=foo&v=bar
will be transformed into:
var filter = new Filter { Operator = FilterOperation.Equals, PropertyName = "foo", Value = "bar" }; // LINQ Translation: myCollection.Where(x => x.foo == "bar");
or to apply multiple filters
- Valid Example: ?o=Equals&p=foo&v=bar&o=Equals&p=fizz&v=buzz
Since this query does not provide a logical operator, parser will choose
AndAlso
which is the default logical operator.
will be transformed into:
var filterOne = new Filter { Operator = FilterOperation.Equals, PropertyName = "foo", Value = "bar" }; var filterTwo = new Filter { Operator = FilterOperation.Equals, PropertyName = "fizz", Value = "buzz" }; // LINQ Translation: myCollection.Where(x => x.foo == "bar" && x.fizz == "buzz");
- Valid Example: ?o=Equals|OrElse&p=foo&v=bar&o=Equals&p=fizz&v=buzz
will be transformed into:
var filterOne = new Filter { LogicalOperator = LogicalOperation.OrElse, Operator = FilterOperation.Equals, PropertyName = "foo", Value = "bar" }; var filterTwo = new Filter { Operator = FilterOperation.Equals, PropertyName = "fizz", Value = "buzz" }; // LINQ Translation: myCollection.Where(x => x.foo == "bar" || x.fizz == "buzz");
- Valid Example with ascending sort and pagination: ?o=Equals&p=foo&v=bar&s=foo,asc&offset=0&count=10
DynamicQueryOptions Transform:
var filter = new Filter { Operator = FilterOperation.Equals, PropertyName = "foo", Value = "bar" }; var sort = new SortOption { PropertyName = "foo", SortingDirection = SortingDirection.Asc }; var pagination = new PaginationOption { Offset = 0, Count = 10 }; /* LINQ Translation: myCollection.Where(x => x.foo == "bar") .OrderBy(ord => ord.foo) .Skip(0) .Take(10); */
- Valid Example of Collection Member Querying ?o=any&p=foo&v=(o=Equals&p=fizz&v=buzz)
will be transformed into:
var filter = new Filter { Operator = FilterOperation.Any, PropertyName = "foo", Value = new DynamicQueryOptions { Operator = FilterOperation.Equals, PropertyName = "fizz", Value = "buzz" } }; // LINQ Translation: myCollection.Where(x => x.foo.Any(y => y.fizz == "buzz"));
- Valid Example with pagination: ?offset=0&count=10
- Valid Example with descending sort: ?o=Equals&p=foo&v=bar&s=foo,desc
- Valid Descending Sort Example without any filters: ?s=foo,desc
Tip: if you do not provide any sorting direction, DynamicQueryBuilder will sort the data in ascending order.
- Valid Example with ascending sort without stating the direction: ?o=Equals&p=foo&v=bar&s=foo
9.5 Operation Shortcodes
DQB has default operation short codes for shorter HTTP queries which are below;
{ "eq", FilterOperation.Equals }, { "lt", FilterOperation.LessThan }, { "cts", FilterOperation.Contains }, { "ne", FilterOperation.NotEqual }, { "ew", FilterOperation.EndsWith }, { "sw", FilterOperation.StartsWith }, { "gt", FilterOperation.GreaterThan }, { "ltoe", FilterOperation.LessThanOrEqual }, { "gtoe", FilterOperation.GreaterThanOrEqual } { "any", FilterOperation.Any } { "all", FilterOperation.All }
9.5.1 Custom Operation Shortcodes
You can change any operation shortcode to whatever you want in
DynamicQueryBuilderSettings
object's CustomOpCodes
member like below.
var mySettings = new DynamicQueryBuilderSettings { CustomOpCodes = new CustomOpCodes { { "my_eq", FilterOperation.Equals }, { "my_lt", FilterOperation.LessThan }, { "my_cts", FilterOperation.Contains }, { "my_ne", FilterOperation.NotEqual }, { "my_ew", FilterOperation.EndsWith }, { "my_sw", FilterOperation.StartsWith }, { "my_gt", FilterOperation.GreaterThan }, { "my_ltoe", FilterOperation.LessThanOrEqual }, { "my_gtoe", FilterOperation.GreaterThanOrEqual }, { "my_any", FilterOperation.Any }, { "my_all", FilterOperation.All }, } };
9.6 Web Action Examples
DynamicQueryAttribute is the handler for parsing the querystring into DynamicQueryOptions class and has 3 optional parameters.
DynamicQueryAttribute( // Declares the max page result count for the endpoint. int maxCountSize = 100, // Declares the switch for inclusion of total data set count to *PaginationOptions* class. bool includeDataSetCountToPagination = true, // Declares the behaviour when the requested page size exceeds the assigned maximum count. PaginationBehaviour exceededPaginationCountBehaviour = PaginationBehaviour.GetMax, // Resolves the dynamic query string from the given query parameter value. string resolveFromParameter = "")
The ResolveFromParameter
This argument exists because some API's would want to send their queries inside of a HTTP parameter like below:
https://foobar.com/results?dqb=%3Fo%3Deq%26p%3Dfoo%26v%3Dbar
So, you can set this parameter specifically for an endpoint with the
DynamicQueryAttribute
or you can set it in DynamicQueryBuilderSettings
globally with QueryResolvers. Check out Query Delivery Methods.
- PaginationBehaviour enum
public enum PaginationBehaviour { // DynamicQueryBuilder will return maxCountSize of results if the *Count* property exceeds *maxCountSize*. GetMax, // DynamicQueryBuilder will throw MaximumResultSetExceededException if the *Count* property exceeds *maxCountSize*. Throw }
- Example with no pagination specified(default pagination options will be applied).
[DynamicQuery] [HttpGet("getMyDataSet")] public IActionResult Get(DynamicQueryOptions filter) { IEnumerable<MyObject> myDataSet = _myRepository.GetMyObjectList(); return Ok(myDataSet.ApplyFilters(filter)); }
- Example with default pagination options for the endpoint specified.
[HttpGet("getMyDataSet")] [DynamicQuery(maxCountSize: 101, includeDataSetCountToPagination: true, exceededPaginationCountBehaviour: PaginationBehaviour.GetMax)] public IActionResult Get(DynamicQueryOptions filter) { IEnumerable<MyObject> myDataSet = _myRepository.GetMyObjectList(); return Ok(myDataSet.ApplyFilters(filter)); }