This project contains of two main parts:
- main package: HamsterWheel.HLinq that main implementation of HTTP Linq resource language
- HamsterWheel.HLinq.Abstractions package that can be used to further develop extensions for HLinq and adding functionalities missing from main package
- HamsterWheel.HLinq.AspNet package contains helpers for HLinq integration with Asp.NET core framework
- HamsterWheel.HLinq.PgSql package with PostgreSql specific functions for use in HLinq queries
- HamsterWheel.HLinq.Client package with fluent API for building HLinq queries on top of HttpClient
Navigation:
- How to use on server
- How to use on the client
- Extensions
- [Dynamic object transformation]
- Roadmap
Below you can find instructions how to get you started using HLinq on the server using HamsterWheel.HLinq.AspNet package.
First, reference AspNet package:
After that add initialization of HLinq:
To actually make it useful you need to add it to the endpoint. In example if you fetch data from the database in this way:
You need to add HLinqQuery<MyEntity> model into your endpoint which is actual HLinq query mapped from HTTP Query String into type safe structure. MyEntity type is important to be the actual type you intend your users to query.
So in above example it will become:
And that is it! You can now call this endpoint with HLinq query filters, ordering, paging and selects!
If you do not use minimal APIs of Asp.Net Core you can use HLinq within controllers too. First you need to install package and configure it inside the host. Those are similar steps.
Reference AspNet package:
Initialize HLinq:
Then you must add HLinqQuery<MyEntity> into your endpoint method:
And that is all. Very similar to minimal APIs and also very simple.
If you are using HLinq entirely within bounds of one application, there is no default limit of how many records you will fetch. And it is not really a problem. You can do that too with just Linq to SQL, i.e. call ToArray or ToList on entire DB collection. But on the API side you do not really have control on what yours users will do and calling and API endpoint without any query modifiers, me personally it is first thing I do, just to see what kind of result it will return, with what information. Since HLinq requires skip[].take[] to be provided this would cause entire data to be serialized and sent to the client. At once. This would be very problematic. Of course this is also not desired by 99% of cases. To fix this HLinq limits records returned to maximum value of 1k. If you do not like the default limit you can change it:
This will limit number of records returned by default to 100.
If a user is trying to fetch a huge quantity of data from the API by specifing i.e. take[1000000], this may overload your db and API server(s). To remedy this, HLinq is overriding those values with IHLinqOptions.HttpDefaultMaxTakeRecords value. If you want to specify a different value for that case change this inside ConfigureHLinq method:
In Linq you can use DB functions from inside the C# code. Those functions are not evaluated on the .net side but translated in SQL and evaluated by the DB engine. HLinq allows you to use them, but it is not aware of your environment. To tell HLinq about your DB capabilities, you need to configure it inside ConfigureHLinq method call. For example, the following line adds PgSql specific functions to HLinq:
HLinq have a client that allows you to build HLinq queries in a more type safe manner using Expressions syntax of Linq. Not full Linq is supported in HLinq (mostly because limited support of URLs characters), but many of filtering methods, ordering, selects, skip, take directives are possible. Using any specific client is not required, though. You can use any HTTP client with Query String support.
HLinq syntax is very similar to Linq syntax. It contains roots, that are equivalent to Linq methods (like Where, Select, OrderBy, Skip, Take) which are parametrized by providing [] with appropriate arguments values. Supported roots are:
- where
- select
- skip
- take
- count
- orderBy
- orderByDescending
- thenBy
- thenByDescending
Roots are chained together using . character. For example:
Casing in root names is not important. For example, WHERE and where are equivalent. Or example even wHeRe is valid to harder to read. The same applies to property names, though JSON property names are preferred. So for example x.firstName is preferred when x.FirstName is also possible and it is a valid CLR property name.
Roots order matters. For example, if you are querying Person type that have FirstName property, querying the API with where[x.firstName==John].select[name=x.firstName] will first filter the collection selecting all the persons with firstName being John and then will select only firstName property as new name property. Resulting json will be:
On the other hand, when you will query API with select[name=x.firstName].where[x.firstName==John] query will return an error.
This is because firstName is property of Person type, but when you applying select[name=x.firstName] you are effectively changing IQueryable to being a collection of new type. C# equivalent of this query would be:
As you can see, FirstName does not exist in a queryable collection anymore.
Parameters of roots depend on the root.
For example, count root does not take any parameters. The only valid usage is count[].
take and skip roots single number as a parameter (i.e. take[10], skip[20]).
orderBy and orderByDescending roots take a single parameter, which is a property name to order by.
select root takes unspecified number of parameters, separated by ,, parameters with two variants:
- select[x.name] selects property as is
- select[newName=x.name] renames selected property.
select allow to select as many properties with both. The order of properties or renames is not important.
where root also takes unspecified number of parameters. Parameters are separated by ,. Syntax depends on the following:
- type of property
- comparison operator or comparison method
- value you are comparing to
For example, when you are comparing to constant value:
But if you want to filter a property containing a specific value:
The same can be achieved with an EF method call:
or the same can be achieved with string.Contains(property, StringComparison.InvariantCultureIgnoreCase) (or similar instance of a property type) method:
Constant value within where parameters can be almost any value. How it is treated depends on the type of the property. I.e. int will be converted to int before comparison. String will not be converted and will be taken as is from Query String.
It is possible to use () in where. This effectively allows grouping of conditions. For example
will return either record with Id=1 or records with Name starting with d when date of birth is after 2010-08-31 00:00. But
will return either with Id=1 OR records with Name starting with d AND date of birth is after 2010-08-31 00:00. This works the same as in Linq.
Constant values do not need to be quoted. If you are looking for a person with their full name, put space in a string between first and last name:
White space is not important in HLinq. You can almost use any white space you want. It is mostly ignored by the parser, except for constant values provided by the user. For example, leading or trailing white space before or after the root is allowed.
You can put white space between root and [ character:
Only space is allowed, but other white space characters are allowed also. For example:
This means that for longer queries you can use new lines to make it more readable.
Lets use demo endpoint /demo/memory that returns SuperHero type data.
returns data as is, which one exception being the maximum limit of records returned (by default 1k).
Adding take[10] instructs HLinq to return only 10 records.
Adding skip[20].take[10] instructs HLinq to return only 10 records after skipping first 20 records.
This allows for simple and expressive paging of data on the client side. I.e., if the default page size is 10 rows, call the endpoint with:
To count total number of records in the collection you are querying you need to use count[] root.
Count can be applied after filtering (or any other root for that matter) to get only the number of records in a filtered collection instead.
This will give you the number of records in the collection that have name property containing Ant value.
There are various filters possible in HLinq:
- equals
- not equals
- greater
- greater or equal
- lesser
- lesser or equal
- string contains (with case-insensitive variant) - usually not supported by DB
- string starts with (with case-insensitive variant) - usually not supported by DB
- string ends with (with case-insensitive variant) - usually not supported by DB
- EF DbFunctions
If this is not enough, you can write your own extension to support new operators or functions.
In example to look for entity with specific Id:
Notice double = sign in comparison. It is consistent with how Linq works in C#. Single equals (=) sign is used in selectors (select[NewName=x.Property]) only.
To filter by records that do not have value use null keyword:
To filter by floating point numeric you can use . delimiter for fractions. It is consistent with CultureInfo.InvariantCulture numerical format, which HLinq is using by default.
HLinq does not apply any precision for equality comparison, so due to floating point rounding errors you may get different results. To fix this you can use two values with > and < operators.
For Date Time and Date Time Offset types you can use ISO 8601 format:
Or with the time zone part:
Equals can be used inside a group as any other filtering condition:
Usage is the same as for equals, but with != operator.
Not equals comparison operator is != which is consistent with C# syntax of Linq.
Usage is the same as for equals, but with > operator.
Usage is the same as for equals, but with >= operator.
Usage is the same as for equals, but with < operator.
Usage is the same as for equals, but with <= operator.
Testing for string contents is a bit different but still resembles Linq syntax.
In HLinq you have to query it like below instead:
You can use the white space in the string:
Warning
Though this is supported by HLinq and Linq, it is not supported by EF. Use it for memory collections only.
Of course usage of string.Contains is consistent with Linq and is case-sensitive. To use a case-insensitive version, you need to add an extra parameter just like in C#:
Also, it is possible to use DB functions in case-insensitive searches:
This is equivalent of a Linq query:
Warning
Though this is supported by HLinq and Linq, it is not supported by EF. Use it for memory collections only.
In the same manner as Contains can be used StartsWith method:
To use a case-insensitive version, you need to add an extra parameter just like in C#:
Warning
Though this is supported by HLinq and Linq, it is not supported by EF. Use it for memory collections only.
In the same manner as Contains and StartsWith can be used EndsWith method:
To use a case-insensitive version, you need to add an extra parameter just like in C#:
Warning
Support for those functions vary between DB engines and their EF providers. Check which ones are by your DB engine and EF.
Warning
This is supported by HLinq and Linq and EF but cannot be used on memory collections. Use it for DB collections only.
This is very similar to string.Contains(str, StringComparison.InvariantCultureIgnoreCase) but instead of using .net runtime function it is translated to db function, and it is applied by DB engine. For example:
This is equivalent of a Linq query:
Very similar to regular ILike but allows for a third argument: escape character.
This is equivalent of a Linq query:
Selecting is used to specify which properties should be returned to the client. It is done by specifying a comma-separated list of property names after the select keyword. It is possible to rename properties by specifying new name with newName=propertyName syntax. For example:
This is equivalent of a Linq query:
This will return only Name and Age properties of each person only.
If you want to rename Name property to FullName then you do:
This, in turn, usually translates to a Linq query as follows:
There is possibility to add constant value to the property:
It can be useful when real property was filtered by select, but a client requires it, i.e., for serialization. It can be also used to achieve compatibility on the client when a server data model is changed without changing the actual code.
HLinq creates a new type on the fly to be serialized to JSON and returns it to the client. This means that other properties of the original type are not returned. Selecting is not an exclusive operation. You need to be specific by providing which properties you are interested in. You cannot exclude properties by excluding them from the query.
If you need to fetch data in a specific order, you can use orderBy and orderByDescending roots. This orders a collection by Name property in ascending order:
There reverse order use orderByDescending root instead.
It is possible to order by more than one property at a time. To do this use any number of thanBy or thanByDescending roots after orderBy or orderByDescending roots.
HLinq have dedicated C# client built on top of HttpClient class. It is available in the HLinq.Client package. To use it, you need to create an instance of this HttpClient with all the necessary configurations: your API url, authentication, retry policies, etc. In case of the demo project it can be done as follows:
Then you can call an extension method that allows you to build HLinq GET query with Linq syntax:
The first argument is the path to the endpoint you want to call. The Second argument is the Linq expression that will be translated to HLinq query.
In the above example it will be translated into:
Noticed that value of the result variable is an int. This is because the builder expression returns an int type. If a builder query returns a collection or any other type, GetWithHLinq will automatically deserialize server JSON response into that type.
If you change the type by doing a custom select operation, the return type will be changed accordingly.
The above query will return an array of strings. On the other hand, if you will ask for custom select which require anonymous type:
JSON response will be deserialized into an array of those objects with single N property of a type of string.
The serialization is done by System.Text.Json library with default settings of JsonSerializerDefaults.Web. If you do not like default serialization rules, you can change them by providing your own JsonSerializerOptions instance to the builder factory For method.
HLinq architecture allows for easy extension of many aspects of the library. HLinq is designed around interfaces retrieved from DI container. If you do not like default behavior, you can provide your own implementations of those interfaces. Just be careful! You might break something :)
For example, HLinq queries can be translated to different languages. Linq syntax is pseudo english. You can translate it to you own language by providing different implementations of ITokenPossibility interfaces. Almost each ITokenPossibility implementation poses TokenValue constant that represent this token in the query. If you want to change how select token is represented in the query, you can provide different implementation of ITokenPossibility interface. Or even better use TokenPossibility<T> class. In Polish, you would write wybierz instead of select, so new implementation would look like this:
Then add it to the DI container:
This adds new select token characters in new language, wybierz. This works along the old syntax, so both are valid:
If you want to replace the token instead, use:
This will cause select[x.Name] to be invalid and an attempt to use it will result in HTTP 400 error with the following response:
It is worth to mention that HLinq does not require Ascii characters only. You can use any characters you want. Translation does not have to be only letters. For example, for sorting/ordering you may choose more expressive Unicode characters:
- ↓ for orderByDescending
- ↑ for oderBy
Then orderBy token possibility can look like this:
And to order you call your API with:
It is also possible to extend HLinq by writing custom expression converters. For example, if you need very complex filtering rules that require one or two simple parameters, you can write a custom converter. Let's say you want to find a person by their full name and your model (like in /demo/db endpoint) does not have a FullName property. You can write a custom converter that will take one parameter and will use them to build a custom expression.
Expression returned when name of the method is hasFullName is equivalent to:
If you will replace default implementation of IStaticMethodToExpressionConverter and register yours:
then you can call your endpoint with:
For the PgSql it will result in SQL
and API will return one record:
Main HLinq package also exposes one helpful extension method of an object type: ExecuteHLinq. This method allows you to execute HLinq queries on any object, for example, to query for a specific property or just some subset of properties. Consider the following line of code:
This returns the value of the Name property of the object.
This can be also achieved with shorter query syntax that implies select and is equivalent to above:
ExecuteHLinq method is also available on collection types.
Collection item can be complex type too:
This would be equivalent to:
You are able to use more than one operation in the query:
This, in turn, would be equivalent to:
- Add support for grouping
- Add support for nested counts [NumberOfAddresses=x.Addresses.Count[]]
- Add support for nested type selects: select[Addresses=select[a.Street,a.City]]
- Add support for methods in select: select[reverse(x.Name)]
- Add support for other than bool methods in filters: where[Distance(x.DateOfBirth, 2025-10-05)<=10]
- Add support for property operation in select: select[FullName=x.Name+' '+x.Surname]
- Add support for other DBs
- Add support for changing grammar rules
.png)


![I860 Intel took a RISC: it did not end well [video]](https://www.youtube.com/img/desktop/supported_browsers/firefox.png)