A bit of history
I remember the day when I started writing the HLinq query language. It was weekend and I was experimenting with writing Single endpoints for Hamster Wheel. Single endpoints were almost done and next step was to write List endpoint. This would allow to get list of records of specific type. After I got single GET returns correct data for chosen record id, list should not be hard to implement. Then just create, update and delete. Basic CRUD API in Hamster Wheel would be finished. I was thrilled that I actually was able to do such thing.
But then I started writing List and when I got first response… it hit me.
How the hell I am going to filter the records?!
The need for some unified way of dealing with at least the filtering was very apparent. Actually one of the reasons I started working on this was because during implementing simple CRUD API for my smart home automation of Nibe heat pump I was implementing filtering of temperature history: by day, a week and month.
So I need filtering.
Also I need paging. Obviously! No system presents data without some kind of paging. Even if this is infinite scroll.
Yes, there are already existing query languages, like Graph QL for example. But I did not had good time working with it. It felt to complicated. Yes, it very powerful in a manner of possibilities you are allowed there as an user. From other point of view on the server side it may be a reason you go bald when some users are killing your API by writing very expensive DB queries. But I did use it on one project and I did not got good time writing the queries. Syntax felt very foreign to me. Maybe it was partially because I did not got good IDE helper – it was just very big string that I had to provide to the HTTP client. But it did not felt like a right way to integrate it as a first query language in the Hamster Wheel.
I wanted something much simpler that could be used via browser address bar. Integration with web UI grid for presenting data would be very nice. Having a way of putting “I want contacts that starts with ‘Aleks'” and having a grid being automatically filtered by that value in “Contact Name” column was one of the goals.
On other project I did use Dynamic Linq. That would be doable. But again I did not had good time working with that library either. Syntax seems to be too much C# crammed inside the string.
var example4 = list.AsQueryable()
.Where("!np(City.Name.ToLower(), \"\").Contains(@0)", "london")
.ToList();
What is ‘!np’? Why I have to provide it? Why ‘@0’? Ok it probably have something to do with parameters being external user input – so unsafe and need to be escaped or sanitized. That is sensible but why during implementation need to think about that kind of stuff? It is sanitized and passed as parameter to DbCommand by Entity Framework any way. Why it have to be done by me passing parameters to extension method. Where
Ah it was probably because of parameter types. If everything would be a string Dynamic Linq would have to deal with conversions by itself.
Ok but then implementation would look like that:
- parse the query of some kind
- if this would be Dynamic Linq it would be very C# like so very foreign to people that do not deal with C# or programming all together
- if this would be something else then I need to invent another language that would be translated to Dynamic Linq
- extract user provided search parameters
- extract properties those parameters would have to applied to
- check the types of both and apply conversion so you can actually call Dynamic Linq query
- then take all that and create query
If I have to invent new query language syntax, parse types and properties and write conversions of string values to appropriate types. This is like half of work of writing new query language.
The other part consists of actually applying all of it to the query and returning response to the API user. And half of it would be dealing with Dynamic Linq strings. For example if you want to do case insensitive search, you have to build different query than for case sensitive search. So Where query parameter string would be different. But you have to take account types, to search for valid property name. I would not force, for example, to the user to remember property names casing. So you have to have case intensive search of property inside the type to be able to write correct Dynamic Linq. But then you would basically end up with code similar to below:
var property = type.GetProperty(propName);
if (property is null)
{
throw new PropertyNotFoundException(propName);
}
if(property.Type == typeof(string))
{
if(caseSensitiveSearch)
{
return property.Name + ".Contains(" + value + ")";
}
else
{
return property.Name + ".ToLower().Contains(" + value.ToLower() + ")";
}
}
else if (property.Type == typeof(int)
{
//handle int search operators ==,!=,>,<,>=,<=
}
else if (property.Type == typeof(DateTime)
{
//handle search operators ==,!=,>,<,>=,<=
}
//handle other types, combinations of string values and conversions to property types
//handle nested properties, methods etc
This is feels like having to deal with half of applying the query to by your self anyway. Then only thing you can get out of Dynamic Linq is that you do not have deal with writing IQueryableExpressions tree syntax on your own.
But I already did in Delegates Factory. So it is not unknown to me.
What was a big unknown was writing language parsers, tokenizer and similar stuff that is very close to compilers. And it seemed like fun. I did a little research on how to do it and it felt like something I would want to try to do on my own.
Even if this would be only to check – can I do it?
I had actually browser page open with an article What is a programming language parser?. And I deliberately did not read it – just to check AFTER how did I do (seems like I wrote Top-down, leftmost parser, not the most complex solution, but HLinq is not most complex query language out there too 🙂 )
From the other point of view I never did use for anything serious and parsing a string would be exactly the place where you use Span<>Span<char> for inspection of string contents. So again: something new and fun to do!
So this felt like a decision was made: I will going to build myself new query language for my Hamster Wheel platform.

Why “HLinq” name?
The main reason for building this new query language was to use it my new API. So obviously HTTP will be used.
And this query language will be used to integrate with Entity Framework and IQueryable interface. So basically Linq.
I was using Linq to Db before Entity Framework was even a thing. So Linq to something, and in this case something is HTTP Api type of record, represented by API path and your own Json Schema that you provided to Hamster Wheel API.
So Linq to HTTP.
HLinq.
At that point in time I was using Abs(olute) Platform name for Hamster Wheel, but now with Hamster being part of the name “H” in HLinq seems appropriate.
HLinq syntax
When I made decision to make my query language as Linq to HTTP, it became obvious that its syntax must be translation of C# lambdas in the HTTP Url character set.
So I looked at documentation of what characters are allowed in the Url. The idea was to have set of character that will be used in query and at the same time does not need to be unencoded. But this needs to be fairly easy to use.
There is RFC-3986 for URI scheme. But it is just long text file so lets refer to this SO question instead 🙂
Lets start with general operations you can do in Linq. Usually you are searching through a collection with Where, transforming data with Select and when this data is presented to the user via UI you usually order it by or limit the set of records with OrderBySkip(x).Take(y). So those operations need to be available in HLinq too.
var persons = queryable.Where(p => p.Age >= 18).Select(p => p.Name).ToArray();
In theory whole Linq expression: .Where(p => p.Age >= 18).Select(p => p.Name) should be possible to pass via url without a problem, even if > and < are not reserved RFC-3986 characters you can still write them in the browser address bar and open the page.
But this seems to be to complicated. p=>p seems redundant. In C# you have specify set of parameters in the expression. This is understandable. But HLinq is not C# in URL. So something like p.Name would be enough. If you are calling persons endpoint like below
GET /api/persons
You already are in the context of a Person type, so specifying it in the query one more time is not helpful but complicates stuff for the user. Lets remove it.
GET /api/persons.Where(p.Age >= 18).Select(p.Name)
In URL case of characters usually does not matter. Of course you can code you WWW server to recognize the difference but, usually it does not. So lower case character would be mostly used.
GET /api/persons.where(p.age >= 18).select(p.name)
This syntax would be fine and really close to the C# syntax. With one slight problem. In Linq you are able to execute methods. List of method parameters also are using circle braces. In C# it does not present a problem since you are using IDE with syntax highlighting, you can write your query in multiple lines and etc. It is still pretty easy to read. But with HLinq it will be compressed in one, very often, long line, without much of white space. That makes it much more harder to understand.
Also in HLinq methods will be used inside Where or Select methods, which are totally different operations from the perspective of HLinq – they are not really methods.
If you look at RFC document, there are two sections for delimiters (called delims in document):
reserved = gen-delims / sub-delims
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
General delimiters consists of [ and ], and sub-delimiters consist of ( and ).
If HLinq would borrowed that distinction so query looks like:
GET /api/persons.where[p.name.contains(Kowalski)].select[p.name]
That is pretty nice. At least for me pretty easy to understand what is the intended behavior after just one glance.
Of course still things may get a bit more complicated when you include condition groups for example. Usually in programming languages you group conditional statements with circle braces and (. HLinq is no different in that regard. )
GET /api/persons.where[(p.name.contains(Kowalski)&&p.name.startsWith(Jan))||p.dateOfBirth>2010-01-01]
This looks more complicated. But so as any code gets more complicated with each conditional operation you need to do to query the data you need. There is no easy way around that. But groups of conditional operators, whether it is in math or in programming languages is usually done with circle braces. And so I did decided to do the same in HLinq.
Of course if you do HLinq query from some programing language or via some tool you can add some white spacing for easier readability. For example above condition can be represented as:
/api/persons.where[
(
p.name.contains(Kowalski)
&& p.name.startsWith(Jan)
)
||
p.dateOfBirth > 2010-01-01
]
This is a bit easier to read and understand query logic. But this won’t work with the browser address bar.
In current, first version of HLinq query syntax looks like below.
query = array-result-query *("." array-result-query ) *1( count )
array-result-query = select | where | 1( ordering-query ) | skip | take
ordering-query = order-by | order-by-descending 1*( "." 1( then-by | then-by-descending ) )
select = "select[" prop-list "]"
prop-list = prop *( "," prop )
prop = "x." name *( "." name )
name = ALPHA *( ALPHA | DIGIT | "_" )
where = "where[" filter *( logical-op filter ) "]"
filter = prop | group | method
group = "(" filter *( logical-op filter ) ")"
method = db-method | prop-method
prop-method = prop "(" 1( constant ) 1*( "," constant ) ")"
db-method = name "(" 1( prop ) 1*( "," constant ) ")"
constant = string | quoted-string | int | float | date-time | quoted-date-time
string = 1*( ALPHA | DIGIT | UNICODE )
quoted-string = quote string quote
quote = "\"" | "'"
int = 1*( DIGIT )
float = 1*( DIGIT ) "." 1*( DIGIT )
date-time = date *1( time )
date = 4( DIGIT ) "-" 2( DIGIT ) "-" 2( DIGIT )
time = "T" 2( DIGIT ) ":" 2(DIGIT) ":" 2( DIGIT ) *1( miliseconds ) *1( zone )
miliseconds = "." 7( DIGIT )
zone = "+" 2( DIGIT ) ":" 2( DIGIT )
quoted-date-time = quote date-time quote
logical-op = "&&" | "||"
order-by = "orderBy[" prop "]"
order-by-descending = "orderByDescending[" prop "]"
then-by = "thenBy[" prop "]"
then-by-descending = "orderByDescending[" prop "]"
skip = "skip[" int "]"
take = "take[" int "]"
count = "count[]"
Few clarifications to above grammar notation:
|means ‘or’.1( )means group have to occur only one time – no more, no less1*( )means group have to occur one or more times*1( )means group have to occur at least one time*( )means group is optional and can be repeated unspecified number of times
HLinq architecture
Applying HLinq query string to IQueryable is done few steps.
IHLinqTokenizerproducesIToken[]array from input stringIHLinqParserproducesIHLinqQuerywhich is tree-like structure of groups of tokensIHLinqQueryApplierappliesIHLinqQueryto sourceIQueryabletransforming it to result of the equivalent Linq query result
Each of those steps consists of multiple substeps. Each of those substeps is configurable by appropriate interfaces.
Tokinizer step
IHLinqTokenizer, HLinq implementation of first query processing pipeline step, works based on ITokenPossibility interface implementations for each of IToken implementation. Each ITokenPossibility implementation have its own token grammar rules based on which assigns if currently considered substring of HLinq query string may be specific token type or not.
For example if query string is “select[x.name]" IHLinqTokenizer asks all ITokenPossibility implementations to give possibility of their tokens being in current range. If current range is 0 to 1, substring is "s". Where token possibility is at that point 0% because Where token expected value is not starting with "s". Select token possibility at that point is:
50% + 50%*("s".length / "select".length) = 50% + 50% * (1/6) ≈ 58,3%
IHLinqTokenizer goes through every ITokenPossibility and discards those that gives 0%. Then advances range to next character. In this case it would be "se". At this point all of other tokens should be discarded (none other tokens can be starting token and starts with "s"), so IHLinqTokenizer advances range to "sel", "sele", "selec" and finally to "select" at which point possibility of Select token should be 100% unless next character is not "[". If this happens (i.e. typo and user sent "selecte[x.Name]"), possibility drops to 0% and tokenizer throws an error – query could not be tokenized and is invalid. If next character is valid then Select token is created and added to result collection of tokens. At the same time current range of string is 6..7 of value "[". Tokenizer again goes through entire ITokenPossibility collection and discards those that gives value of 0%.
query: "select[x.name]" | ||||
| Range | Substring | Next Character | Select Possibility | Where Possibility |
| 0..1 | s | e | ~58% | 0% |
| 0..2 | se | l | ~67% | 0% |
| 0..3 | sel | e | 75% | 0% |
| 0..4 | sele | c | ~83% | 0% |
| 0..5 | selec | t | ~92% | 0% |
| 0..6 | select | [ | 100% | 0% |
For another query, that shows better next character value consideration, let us consider "orderByDescending[x.id]"
query: "orderByDescending[x.id]“ | ||||
| Range | Substring | Next Character | OrderBy Possibility | OrderByDescending Possibility |
| 0..1 | o | r | ~57% | ~53% |
| 0..2 | or | d | ~64% | ~56% |
| 0..3 | ord | e | ~71% | ~59% |
| 0..4 | orde | r | ~79% | ~62% |
| 0..5 | order | B | ~86% | ~65% |
| 0..6 | orderB | y | ~93% | ~68% |
| 0..7 | orderBy | D | 0% | ~71% |
| 0..6 | orderByD | e | 0% | ~74% |
| 0..8 | orderByDe | s | 0% | ~77% |
| 0..9 | orderByDes | c | 0% | ~79% |
| 0..10 | orderByDesc | e | 0% | ~82% |
| 0..11 | orderByDesc | e | 0% | ~82% |
| 0..12 | orderByDesce | n | 0% | ~85% |
| 0..13 | orderByDescen | d | 0% | ~88% |
| 0..14 | orderByDescend | i | 0% | ~91% |
| 0..15 | orderByDesceendi | n | 0% | ~94% |
| 0..16 | orderByDescendin | g | 0% | ~97% |
| 0..17 | orderByDescending | [ | 0% | ~100% |
As you can see possibility of OrderBy token being in the beginning of the query is greater then OrderByDescending. This continues as tokenizer advances analyzing the query to the point when it encounter "D" after "orderBy" – then OrderBy ITokenPossibility drops possibility of its token to 0% because next character must be square bracket: "[".
Those are examples of keyword based tokens, which are tokens that only allows specific string in the query to be considered. There are also other type of tokens, that allows more possibilities: delimited based tokens. Those are , PropertyMethod and . At the tokenizer query processing steps, HLinq does not really now what identifiers are allowed for properties or methods, which is why it looks for ending characters of such tokens.NameOrValue
For example: Method token delimiter is only because method call can only be used with list of parameters enclosed in circle brackets. '('Property token is usually used for conditions with operators like , != , ==>, and others, so its delimiters are respectively: <, !, = and >.<
Let us consider query from first example: . After tokenizer done its work with first for tokens: "select[x.name]", Select, LeftSquareBracket and Entity for Dot substring, it starts to consider rest, but only possible tokens are "select[x."Property and Method at this range.
| Range | Substring | Next Character | Property Possibility | Method Possibility |
| 9..10 | N | a | 50 | 50 |
| 9..11 | Na | m | 50 | 50 |
| 9..12 | Nam | e | 50 | 50 |
| 9..13 | Name | ] | 100 | 0 |
As you can see both tokens are considered by tokenizer till the point of first character that does not match delimiters of one of them. Then only possible token is ']'Property and this is added to resulting list of tokens.
Parser step
Second step in HLinq query processing pipeline is parsing mainly done by implementation. Parser analyzes collection of tokens and creates tree structure of IHLinqParsers that are then transformed into Entity Framework method expression parameters.ITreeElement
Parser, tree elements are similar in concept to tokens, but instead of allowed collection of previous tokens and allowed next characters, tree element:
- have allowed collections of starting tokens
- list of allowed parents
- list of allowed ending tokens
For example tree element requires as starting tokensSelectRoot
SelectandRightSquareToken- or
Dot,SelectandRightSquareToken
It is, of course, because each root can be placed at the start of the query () or after another root tree element ("select[x.name]")."where[x.dateOfBirth>2005-01-01].select[x.name]"
and any another root element must be finished with SelectRoot (RightSquareToken character). Also each root does not have collection of valid parents, because they are root themselves and do not have root branches.']'
Other elements have set of valid parents. For example can be parent of only SelectRoot element. This is because query syntax only allows PropertyAssignment or x.name statements.newName=x.name
Other tree elements have more complicated structure. For example Property can be used from inside the (via SelectRoot) or from PropertyAssignment (inside the WhereRoot element) or be placed inside one of the ordering roots. Or for example Condition is very simple and does not have any children.CountRoot
Full structure of tree elements is represented in below graph.
Parser knows the structure of this graph and knows the requirements of starting and ending tokens for each of those elements. Based on that parser walks through collection of tokens and builds the tree.
For example string will be tokenized as "select[x.name]". Then parser inspects tokens and builds a tree:[Select, LeftSquareBracket, Entity, Dot, PropertyName, RightSquareBracket]
- Only viable root element is
because starting tokens areSelectRoot[Select, LeftSquareBracket] - Parser creates
and removes 0 tokens at the startSelectRoot - Only viable children of this root is
and it have valid starting tokensPropertyAssignment[Entity, Dot] - Parser creates
inside root and removes 2 tokens at the startPropertyAssignment - Viable children of
are:PropertyAssignmentthat requiresPropertytokens[Entity, Dot]that requiresInitializerPropertyNametokens[NameOrValue, Assignment]that requiresInitializerConstValuetokens[Assigment, NameOrValue]
- Only
is possible and parser creates child of that type inPropertyparent and removes 3 tokens at the startPropertyAssigment - The are no possibilities of children of
element; parser finishes element and goes up in the treeProperty - With 1 token left (
) none of the children ofRightSquareBracketare possible; parsers goes up in the treePropertyAssignment is valid finish ofRightSquareBracketelement. Parser removes last token and finishes root element.SelectRoot- With none tokens left, parser ends its work and return valid tree structure to the applier.
The resulting tree will be having following structure (in [] brackets are tokens assigned to tree element) :
Applier step
When parser finishes its tree, it is passed to the . This class in turn does two things:IHLinqQueryApplier
- Iterates root elements
- For each root element create Linq expression tree
- Applies expression by calling appropriate
methodIQueryable - Passes resulting
instance to the next rootIQueryable
This is quite simple. The only complicated thing here is using converting strings to appropriate CLR types and finding out which property and method names are correct.
First thing is done by value converters. Second by expression builders.
For element appropriate applier instance does either:SelectRoot
- select property of an object
- map object to another type
which both are basically the same from logical perspective, but not from .NET Expression Tree perspective.
Mapping object by selecting property is equivalent of following Linq code:
query = query.Select(x => x.Name);
On the other hand mapping to another complex type by selecting set of properties (or renaming some of them) is equivalent to:
query = query.Select(x => new{
NewName = x.Name,
x.Address
});
Of course both are very different expressions even if C# Linq syntax is very similar. First is just single that selects single property of the type. Second is quite complicated MemberExpression that must be called with MemberInitExpression as its first parameter and set of NewExpression as a second one. All of those expressions are created by MemberBindings implementations. Inside those classes if some constant value is referenced (i.e. in IElementToExpressionConverter 18 is constant) before binding it inside expression, first it is converted to the same type as property (for where[x.age>18]age property it would be most probably an integral type).
Applier creates expression tree for user HLinq query using expression converters. Expression converters are using value converters to convert string constants into other types, if needed. After that applier just call correct method. For IQueryable it is SelectRoot extension method IQueryable.Select
public static IQueryable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, int, TResult>> selector)
For this is WhereRoot, for Queryable.Where it is CountRoot and etc.Queryable.Count
Lets consider simple example of HLinq query. Applier for "select[x.name,active=true]" will call SelectRoot that implements interface ElementToExpressionConverter<SelectRoot>. This class will call expression converters for all the children of main root tree element. The result will be similar to the graph below.IElementToExpressionConverter<SelectRoot>
After calling method with source Queryable.Select instance and this expression. The result will be used as source IQueryable instance used by next tree element. If there are no other roots, like in our example, applier calls IQueryable method that forces Enumarable.ToArray to be enumerated. If source is the database EF will translate expressions to SQL and fetch the data. If source is in memory collection, this will cause expressions to be compiled to a delegates and applied to a collection. IQueryable
In above set of transformations applied to the , value converters are used convert IQueryable and "true" to appropriate types. First is not really converted (from string to string) but still this no-op is done by converter. Second is converted to integral value before passed to "10" method.Queryable.Take
Customization and Extensibility
Almost all parts of HLinq services are resolved from . In Asp.NET Core it is the same service provider that is used by the API. That means if you need to modify some parts of HLinq or specific behavior of the package you can just override specific service.IServiceProvider
For example all implementation are resolved from service provider and some of them contains string value of the token it is expecting to find in the query string.ITokenPossibility
public sealed class Possibility(IGrammar grammar) : TokenPossibility<Select>(grammar, "select")
{
protected override bool PreviousTokensMatch(List<IToken> previousTokens) =>
Rule.PreviousTokensMatch(previousTokens);
}
Second parameter of base class is value of the token. So to create translation of the HLinq query syntax you can create your own implementation that have different value at this place. TokenPossibility
I.e. in my language, Polish it would be something like:
public class SelectPossibility(IGrammar grammar) : TokenPossibility<Select>(grammar, "wybierz")
{
protected override bool PreviousTokensMatch(List<IToken> previousTokens) =>
Rule.PreviousTokensMatch(previousTokens);
}
Exactly the same class but with different token string. This way instead of querying with "select[x.name]" you query with "wybierz[x.name]".
Similar way you can DB specific functions or custom filters, by adding/overwriting another service. For DB functions it is . For custom filters IStaticMethodSource.IStaticMethodToExpressionConverter
In example HLinq.PgSql package implementation of is following:IStaticMethodSource
public class PgSqlEntityFrameworkStaticMethodProvider : IStaticMethodSource
{
public Type[] Types { get; } = [typeof(NpgsqlDbFunctionsExtensions)];
}
This means that when HLinq will see use of method inside the filter, it will try to resolve it from the where class. This will cause method NpgsqlDbFunctionsExtensionsILike and other to be recognized and translated correctly to the SQL query.
public static bool ILike(this DbFunctions _, string matchExpression, string pattern)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ILike)));
From other hand if you overwrite with your own implementation you can add your own custom filtering methods. For example right now you can’t use following syntax in filtering conditions:IStaticMethodToExpressionConverter
where[(x.firstName + " " + x.lastName)==John Doe]
At least not directly, but it is possible to accomplish that via custom filter.
public class CustomFilterConverter(IPropertiesCache propertiesCache) : IStaticMethodToExpressionConverter
{
private readonly StaticMethodToExpressionConverter _converter = new();
public Expression BuildStatic(IBuilderContext context, IMethod method, IParametersConverter parametersConverter)
{
if (method.GetName(context.HLinqQuery) != "hasFullName")
{
return _converter.BuildStatic(context, method, parametersConverter);
}
var fullNameSearchConstant = method.Children[1].Tokens[0].GetValue(context.HLinqQuery);
//the below expression is equivalent to:
//LambdaExpression condition = (Person p) => p.FirstName + " " + p.LastName == fullNameSearchConstant;
var stringConcatMethod = typeof(string).GetMethod("Concat", [typeof(string), typeof(string)]);
var firstNamePlusSpace = Expression.Add(Expression.Property(context.Param, propertiesCache.Single(context.Type, "FirstName")!), Expression.Constant(" "), stringConcatMethod);
var firstNameSpaceAndLastName = Expression.Add(firstNamePlusSpace, Expression.Property(context.Param, propertiesCache.Single(context.Type, "LastName")!), stringConcatMethod);
return Expression.Equal(firstNameSpaceAndLastName, Expression.Constant(fullNameSearchConstant));
}
}
Following custom implementation of will cause HLinq to replace IStaticMethodToExpressionConverter"hasFullName(x, John Doe)" HLinq method equivalent expression of p.FirstName + " " + p.LastName == "John Doe".
Those are just 2 examples. You can overwrite much more to make HLinq behave they way you want.
| Interface | Purpose for overwrite |
| Translation |
| Add new conversions for custom types for constant expressions |
| Translation |
, | Custom tree to Expressions conversions |
| Custom DB functions |
| Custom parser rules, change syntax |
| Change tokenizer rules |
| Custom grammar/syntax rules |
| Custom method related Reflection operations/Methods cache |
| Custom property related Reflection operations/Property cache |
Conclusion
HLinq library is easy to use and easy to integrate with existing solutions written in Asp.NET and .NET in general. It is also easy to customize and extend. Its syntax is based on Linq so it should be very easy to understand for every .NET developer. It is also compatible with HTTP Uri standard so it is possible to use it inside the web browser address bar. Syntax is also not that complicated to understand for non-technical people.

