What is query language? SQL is Structured Query Language and you use it to query your database for data you are interested in. It does not have to do anything with your API. If you ever were working with big XML files you probably used XQuery or XPath, query languages designed to XML for data. There are specialized languages that are designed to work with APIs to query them for data. For example, you probably heard about GraphQL or even used it when you were working with some API.
Do you need one for you API?
If you try to use online marketplace search and enter “SPF”, for example, you will see a lot of sun creams in the result. But you are interested in transceivers for your new 100G switch instead. You will try limit search to category of “electronics” or “networking”. In the query string of the browser page you maybe will see something like:
- "?_nkw=spf&_from=R40"
- "k=spf&i=computers&crid=YIPWAZPBD3BT&sprefix=spf%2Ccomputers%2C87&ref=nb_sb_noss"
- "networking-4413?string=spf"
This is very hard to decipher and not a problem if you are developing a website. Most users do not care about those strange signs in the browser address. But if you are developing an API, this does matter. Those “strange letters and numbers” is part of your product! Ease of use for every product is very important. Even if this is very technical product, for technically apt people.
I was working on commercial and non-commercial applications that were trying to reinvent API query language in some way. In example API or website had few query string parameters for filters (like “name” or “date” or “active=true”) then somewhere in application logic it would be translated into SQL query parameters:
dbContext.Persons.Where(p => p.Name.ToLower().Contains(name)).ToList() eventsStorage.Events.Where(e => e.Date.Date == date).ToList(); ordersRepository.Find(o => o.Status.ToString().ToLower() == "active").ToArray();
Which is fine. But when project grows and consists of more parts and more modules, it is hard to keep things consistent and lack of consistency is a problem for users. They do not care if feature or microservice was developed two years ago and still is using ‘old approach ™’. Concise approach to search across your API is much more important.
You can do that using query language. GraphQL, as already mentioned is a possibility, but it also allows your users to create very complex, heavy and time-consuming queries. Personally, I find it very hard to use. There is also Dynamic Linq and I saw it being used in one project I worked for. But probably allowing your users to write dynamic code for your application to run is not the best idea. There are also libraries, like RQL.NET, but when you search for .NET query language you will get results connected to Linq mostly. This means that for .NET simplest solution would be to use Linq or something similar for API query language. This is the logical solution.
HLinq: Linq to HTTP
HLinq is single Nuget package that allows you to add query language capabilities into your API very easily. Syntax is also quite clear to understand, especially if you already used Linq before.
For example filtering is done with following syntax:
where[ x.name == John ]                                                            - search for people with name "John"
where[ x.name != John ]                                                            - search for people with name not being "John"
where[ x.id == 1 ]                                                                 - search for specific record by its Id
where[ x.id == 77774169-BB9D-4DF9-A4A7-52019C4A445D ]                              - the same as above but with GUID instead
where[ x.name == John Smith ]                                                      - name search but with space inside
where[ x.email == null ]                                                           - look for people that have no email set
where[ x.number == 1 ]                                                             - integral value search
where[ x.Number == 1.2323 ]                                                        - exact fractional number value search
where[ x.Number > 1.2322 && x.Number < 1.2324 ]                                    - look for range of numbers
where[ x.placed == 2025-10-05T19:43:07.3693705Z ]                                  - exact date time filter
where[ x.placed==2011-01-30 15:55:20.433+01:00 ]                                   - date time and zone filter
where[ x.dateOfBirth >= 2010-08-31 ]                                               - search records with date greater or equal than specified value
where[ x.name.EndsWith(s) || x.Name.StartsWith(d) ]                                - search with either one condition OR another condition
where[ x.status == active && x.count > 10 ]                                        - both conditions must be TRUE
where[ ( x.Id==1 || x.Name.StartsWith(d) ) && x.dateOfBith >= 2010-08-31 00:00 ) ] - search with AND and OR logical operators applied to conditions or group of conditions enclosed with '(' ')'
where[ x.name.contains(Han) ]                                                      - case-sensitive 'like' search
where[ x.name.contains(han, StringComparison.InvariantCultureIgnoreCase) ]         - case-insensitive search using memory-only collection search
where[ ilike(x.name, han) ]                                                        - case-insensitive search using DB specific function
where[x.name==John]                                                                - white space is optional
where[                                                                             - white space really does not matter!
  (
     p.name.contains(Kowalski)
     && p.name.startsWith(Jan)
  )
  ||
  p.dateOfBirth > 2010-01-01
]
where[ x.name=="John " ]                                                           - except for search parameters values
where[ x.Name.Contains(Jan) ]                                                      - casing of properties and methods does not matter 
Also, very useful for users is ability to order records in a specific way:
orderBy[x.name] - order by name in ascending order orderByDescending[x.name] - the same but in reverse order orderBy[x.age].thenBy[x.lastActive] - order by two properties, first by age, then by last activity date orderBy[x.age].thenByDescending[x.lastActive] - the same but most active users put on top orderByDescending[x.age].thenByDescending[x.lastActive] - the same but oldest users first
It is possible to count number of records in your query:
count[] - count all records where[x.date <= 2020-01-01].count[] - count only records that satisfy condition
If you are building front end or some kind of client of your API, it is necessary to limit number of records returned or only fetch records from specific offset.
take[200] - return only 200 records skip[1000].take[200] - take only 200 records but from offset 1000
There is also possibility to transform response, to limit it to only specific properties that user is interested in.
select[ x.name ] - return only one property select[ x.name, x.age ] - return only two properties select[ fullName = x.name, x.age ] - the same two properties but name will be renamed to "fullName" select[ fullName = x.name, active=true ] - you can add syntactic property to the response where[ x.age > 22 ].select[ fullName = x.name ] - get names of people that are older than specific age select[ fullName = x.name ].where[ x.age > 22 ] - but this will not work because age does not exist after applying "select[]" transformation
All of this is relatively easy to understand on the first glance. Or at least easier to grasp than something like that (RQL.NET):
var rqlExpression = {
  filter: {
    "isDone": 1,
    "$or":[
        {"updatedAt": {"$lt": "2020/01/02", "$gt":1577836800}},
      ],
  },
  limit : 1000,
  offset: 0,
  sort:["-updatedAt"],
}
Or json query of GraphQL:
{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
  }
}
Install HLinq
Using HLinq in your API is quite simple. Install Nuget package in your project:
<PackageReference Include="HamsterWheel.HLinq.AspNet" Version="0.4.0" />
After that add initialization of HLinq, in your Program.csServicesCollection
services.ConfigureHLinq();
Then you need to add HLinqQuery<MyEntity>MyEntity
app.MapGet("/endpoint",
    (DbContext dbContext, HLinqQuery<Superhero> query, CancellationToken cancellationToken) =>
    {
        var queryable = dbContext.MySet; 
        return query.ApplyTo(queryable, cancellationToken);
    });
If you are using Controllers instead of Minimal API you can do this instead:
[HttpGet("my-endpoint")]
public IActionResult GetData(HLinqQuery<MyEntity> query, CancellationToken cancellationToken) =>
    Ok(query.ApplyTo(dbContext.MySet, cancellationToken));
And that is it!
You can now call this endpoint with HLinq query filters, ordering, paging and selects! This will work with db collections and in memory collections or any other collections backed by IQueryable
For example if you have endpoint that responds with the collection of persons serialized from following type:
public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string Email { get; set; } = null!;
    public string? IpAddress { get; set; }
}
If you add HLinq query model to that endpoint, apply it to a collection and return transformed collection to the client.
For example if you will call this endpoint without HLinq query at all, it will return everything all 1000 at most records if collection is bigger. This is a safety mechanism to prevent user from fetching your entire database in few requests (though it may be configured to be different value).
If you call it with:
?count[]
You will get a response that is just an integral value.
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Thu, 30 Oct 2025 09:30:12 GMT Server: Kestrel Transfer-Encoding: chunked 2000
If you call it with:
?where[x.firstName.contains(tra)]
It will return all persons with first name containing ‘tra’.
[
  {
    "id": 546,
    "firstName": "Demetra",
    "lastName": "Kauffman",
    "email": "dkauffmanf5@wufoo.com",
    "ipAddress": "6.84.100.186"
  },
  {
    "id": 965,
    "firstName": "Anitra",
    "lastName": "MacGraith",
    "email": "amacgraithqs@mozilla.org",
    "ipAddress": "241.235.111.149"
  }
]
If you need wildcard search done on DB query side it can be done with DB function. I.e. For PgSql it is "?where[ilike(x.firstName,tra%)]" but since character ‘%’ cannot be placed directly in HTTP url it have to be encoded as %25.
?where[ilike(x.firstName,tra%25)]
This is equivalent to doing EF query like below.
persons.Where(x => EF.Functions.ILike(x.FirstName, "tra%"));
And both will be translated directly to similar SQL:
SELECT p."Id", p."Email", p."FirstName", p."IpAddress", p."LastName" FROM "Persons" AS p WHERE p."FirstName" ILIKE 'tra%' LIMIT @__p_0
And you will get response:
[
  {
    "id": 46,
    "firstName": "Travers",
    "lastName": "Carsberg",
    "email": "tcarsberg19@yandex.ru",
    "ipAddress": "156.239.72.190"
  },
  {
    "id": 1001,
    "firstName": "Tracey",
    "lastName": "Bellard",
    "email": "tbellardrs@wikipedia.org",
    "ipAddress": "145.78.92.113"
  }
]
If collection that your API is exposing to the client is not DB function, similar case-insensitive search can be done with the following query:
?where[x.name.contains(b, StringComparison.InvariantCultureIgnoreCase)]
This is similar to Linq query:
persons.Where(p => p.Name.Contains("b", StringComparison.InvariantCultureIgnoreCase))
Conclusion
HLinq does not offer all the capabilities of Linq, for example grouping for now is not supported (but it is on the roadmap!), but it is pretty powerful tool as it is now. It is also designed to be easily configurable and extensible, but I think the biggest upside is ease of use in already existing Asp.NET application.


