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
FilterOperationandLogicalOperatorproperties ofFilterclass. - Parameter formation should be
FilterOperation|LogicalOperatorusing|as a delimeter between properties. - This parameter can be placed anywhere in the querystring.
- This parameter should be forming a triplet with
pandvparameters.
9.3.2 p Parameter
- Refers to the
PropertyNameproperty ofFilterclass. - This parameter should be placed after the
oparameter.. - This parameter should be forming a triplet with
oandvparameters.
9.3.3 v Parameter
- Refers to the
PropertyValueproperty ofFilterclass. - This parameter should be placed after the
pparameter.. - This parameter should be forming a triplet with
oandpparameters.
9.3.4 s Parameter
- Refers to the
SortOptionclass. - 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
Offsetproperty ofPaginationOptionclass. - 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
Countproperty ofPaginationOptionclass. - 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
AndAlsowhich 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));
}