|
1 | 1 | # Quaero |
2 | 2 |
|
3 | | -A small query language that can transform queries into system-specific filters. |
| 3 | +> A web-friendly query language that transforms into Microsoft Graph, Active Directory, and SCIM filters |
4 | 4 |
|
5 | | -Quaero is designed to be web-friendly by using letter-based operators instead of symbols. It's very similar to Microsoft Graph (OData) and SCIM filter syntax. |
6 | | -See [example queries](#example-queries) for some examples. |
| 5 | +[](https://www.nuget.org/packages/Quaero) |
| 6 | +[](https://github.com/khellang/Quaero/actions) |
7 | 7 |
|
8 | | -This repository contains translation implementations for Microsoft Graph, Active Directory (LDAP) and SCIM filters, as well as in-memory predicates. |
| 8 | +## What is Quaero? |
9 | 9 |
|
10 | | -## Supported operators |
| 10 | +Quaero is a small, intuitive query language designed to unify filtering across different systems. |
| 11 | +Instead of learning multiple query syntaxes for Microsoft Graph, LDAP, and SCIM, you write queries once in Quaero and transform them into the target system's native filter format. |
11 | 12 |
|
12 | | -The language supports a wide range of operators supported by most filter/query languages. |
| 13 | +**Key Benefits:** |
| 14 | +- **Universal syntax** - Write queries once, use everywhere |
| 15 | +- **Web-friendly** - Uses readable operators like `eq`, `gt`, `and` instead of symbols |
| 16 | +- **Type-safe** - Built for .NET with strong typing support |
| 17 | +- **Optimizable** - Automatic query optimization removes redundancies |
13 | 18 |
|
14 | | -### Equality operators |
| 19 | +## Quick Start |
15 | 20 |
|
16 | | -- Equals (`eq`) |
17 | | -- Not equals (`ne`) |
18 | | -- Logical negation (`not`) |
19 | | -- In (`in`) |
| 21 | +### Installation |
20 | 22 |
|
21 | | -### Relational operators |
| 23 | +```bash |
| 24 | +# Core library |
| 25 | +dotnet add package Quaero |
22 | 26 |
|
23 | | -- Less than (`lt`) |
24 | | -- Greater than (`gt`) |
25 | | -- Less than or equal to (`le`) |
26 | | -- Greater than or equal to (`ge`) |
27 | | - |
28 | | -### Conditional operators |
| 27 | +# Target-specific packages |
| 28 | +dotnet add package Quaero.MicrosoftGraph |
| 29 | +dotnet add package Quaero.Ldap |
| 30 | +dotnet add package Quaero.Scim |
| 31 | +``` |
29 | 32 |
|
30 | | -- And (`and`) |
31 | | -- Or (`or`) |
| 33 | +### Basic Usage |
32 | 34 |
|
33 | | -### Functions |
| 35 | +```csharp |
| 36 | +using Quaero; |
34 | 37 |
|
35 | | -- Starts with (`sw`) |
36 | | -- Ends with (`ew`) |
37 | | -- Contains (`co`) |
| 38 | +// Parse a filter from string |
| 39 | +Filter filter = Filter.Parse("age gt 42 and department eq \"Engineering\""); |
38 | 40 |
|
39 | | -## Example queries |
| 41 | +// Or build programmatically |
| 42 | +Filter filter = Filter.And( |
| 43 | + Filter.GreaterThan("age", 42), |
| 44 | + Filter.Equals("department", "Engineering") |
| 45 | +); |
40 | 46 |
|
41 | | -| Quaero | Microsoft Graph | LDAP | SCIM | |
42 | | -|------------------------------------------------|------------------------------------------------|---------------------------------------------------------|-----------------------------------------------------| |
43 | | -| `age gt 42` | `age gt 42` | `(age>=43)` | `age gt 42` | |
44 | | -| `age ge 42` | `age ge 42` | `(age>=42)` | `age ge 42` | |
45 | | -| `age lt 42` | `age lt 42` | `(age<=41)` | `age lt 42` | |
46 | | -| `age le 42` | `age le 42` | `(age<=42)` | `age le 42` | |
47 | | -| `name eq "John"` | `name eq 'John'` | `(name=John)` | `name eq "John"` | |
48 | | -| `not(name eq "John")` | `name ne 'John'` | `(!(name=John))` | `name ne "John"` | |
49 | | -| `department in ["Retail", "Sales"]` | `department in ('Retail', 'Sales')` | `(\|(department=Retail)(department=Sales))` | `(department eq "Retail" or department eq "Sales")` | |
50 | | -| `isRead eq false` | `isRead eq false` | `(isRead=FALSE)` | `isRead eq false` | |
51 | | -| `mail ew "outlook.com"` | `endsWith(mail, 'outlook.com')` | `(mail=*outlook.com)` | `mail ew "outlook.com"` | |
52 | | -| `parent ne null` | `parent ne null` | `(parent=*)` | `parent pr` | |
53 | | -| `name pr` | `name ne null` | `(name=*)` | `name pr` | |
54 | | -| `id eq "275e50ae-ceb8-4f33-9e68-b3b9dc87ea68"` | `id eq '275e50ae-ceb8-4f33-9e68-b3b9dc87ea68'` | `(id=\AE\50\5E\27\B8\CE\33\4F\9E\68\B3\B9\DC\87\EA\68)` | `id eq "275e50ae-ceb8-4f33-9e68-b3b9dc87ea68"` | |
| 47 | +// Transform to target system |
| 48 | +string microsoftGraph = filter.ToMicrosoftGraphFilter(); |
| 49 | +string ldapFilter = filter.ToLdapFilter(); |
| 50 | +string scimFilter = filter.ToScimFilter(); |
55 | 51 |
|
56 | | -## Usage |
| 52 | +// Use in-memory |
| 53 | +Func<Employee, bool> predicate = filter.ToPredicate<Employee>(); |
| 54 | +``` |
57 | 55 |
|
58 | | -Either parse a filter expression from a string using `Filter.Parse` or `Filter.TryParse`: |
| 56 | +## Supported Operators |
| 57 | + |
| 58 | +| Operator | Description | Example | |
| 59 | +|----------|-------------|---------| |
| 60 | +| `eq` | Equals | `name eq "John"` | |
| 61 | +| `ne` | Not equals | `status ne "inactive"` | |
| 62 | +| `gt` | Greater than | `age gt 21` | |
| 63 | +| `ge` | Greater than or equal | `salary ge 50000` | |
| 64 | +| `lt` | Less than | `score lt 100` | |
| 65 | +| `le` | Less than or equal | `rating le 5` | |
| 66 | +| `sw` | Starts with | `email sw "admin"` | |
| 67 | +| `ew` | Ends with | `domain ew ".com"` | |
| 68 | +| `co` | Contains | `title co "Manager"` | |
| 69 | +| `in` | In list | `status in ["active", "pending"]` | |
| 70 | +| `pr` | Present (not null) | `phoneNumber pr` | |
| 71 | +| `and` | Logical AND | `age gt 18 and verified eq true` | |
| 72 | +| `or` | Logical OR | `role eq "admin" or role eq "moderator"` | |
| 73 | +| `not` | Logical NOT | `not(status eq "deleted")` | |
| 74 | + |
| 75 | +## Translation Examples |
| 76 | + |
| 77 | +Here's how the same Quaero query translates across different systems: |
| 78 | + |
| 79 | +| System | Query | |
| 80 | +|-------|---------------------------------------------| |
| 81 | +| **Quaero** | `age gt 42 and department eq "Engineering"` | |
| 82 | +| **Microsoft Graph** | `age gt 42 and department eq 'Engineering'` | |
| 83 | +| **LDAP** | `(&(age>=43)(department=Engineering))` | |
| 84 | +| **SCIM** | `age gt 42 and department eq "Engineering"` | |
| 85 | + |
| 86 | +### Complex Query Example |
59 | 87 |
|
60 | 88 | ```csharp |
61 | | -Filter filter = Filter.Parse("age gt 42"); |
62 | | -``` |
| 89 | +// Quaero query |
| 90 | +string query = @"(department in [""Sales"", ""Marketing""] and salary ge 75000) |
| 91 | + or (role eq ""Senior Developer"" and experience gt 5)"; |
63 | 92 |
|
64 | | -Or manually construct a filter expression using the factory methods on the `Filter` class: |
| 93 | +Filter filter = Filter.Parse(query); |
65 | 94 |
|
66 | | -```csharp |
67 | | -Filter filter = Filter.GreaterThan("age", 42); |
| 95 | +// Microsoft Graph: ((department in ('Sales', 'Marketing') and salary ge 75000) or (role eq 'Senior Developer' and experience gt 5)) |
| 96 | +// LDAP: (|((&(|(department=Sales)(department=Marketing))(salary>=75000))(&(role=Senior Developer)(experience>=6)))) |
| 97 | +// SCIM: (department in ["Sales", "Marketing"] and salary ge 75000) or (role eq "Senior Developer" and experience gt 5) |
68 | 98 | ``` |
69 | 99 |
|
70 | | -Once the filter has been successfully parsed, it can be optimized by calling the `Optimize` extension method. This will take care of removing redundancies and shortening the resulting query. |
| 100 | +## Advanced Features |
71 | 101 |
|
72 | | -To transform the query into an LDAP filter, reference the `Quaero.Ldap` package and call the `ToLdapFilter` method on it: |
| 102 | +### Query Optimization |
73 | 103 |
|
74 | 104 | ```csharp |
75 | | -string result = filter.ToLdapFilter(); |
| 105 | +Filter filter = Filter.Parse("name eq \"John\" and name eq \"John\""); |
| 106 | +Filter optimized = filter.Optimize(); // Removes redundant conditions |
76 | 107 | ``` |
77 | 108 |
|
78 | | -For Microsoft Graph, reference the `Quaero.MicrosoftGraph` package and call `ToMicrosoftGraphFilter` on the filter: |
| 109 | +### In-Memory Evaluation |
79 | 110 |
|
80 | 111 | ```csharp |
81 | | -string result = filter.ToMicrosoftGraphFilter(); |
82 | | -``` |
| 112 | +record Employee(string Name, int Age, string Department); |
83 | 113 |
|
84 | | -And for SCIM, reference the `Quaero.Scim` package and call `ToScimFilter` on the filter: |
| 114 | +var employees = new List<Employee> |
| 115 | +{ |
| 116 | + new("Alice", 30, "Engineering"), |
| 117 | + new("Bob", 45, "Sales"), |
| 118 | + new("Carol", 28, "Engineering") |
| 119 | +}; |
85 | 120 |
|
86 | | -```csharp |
87 | | -string result = filter.ToScimFilter(); |
| 121 | +Filter filter = Filter.Parse("age gt 35 or department eq \"Engineering\""); |
| 122 | +Func<Employee, bool> predicate = filter.ToPredicate<Employee>(); |
| 123 | + |
| 124 | +var results = employees.Where(predicate).ToList(); |
| 125 | +// Returns: Alice, Bob, Carol |
88 | 126 | ``` |
89 | 127 |
|
90 | | -If you want to evaluate a filter in-memory, you can call the `ToPredicate<T>` method on it: |
| 128 | +## Error Handling |
91 | 129 |
|
92 | 130 | ```csharp |
93 | | -record Person(string Name, int Age); |
94 | | -Func<Person, bool> predicate = filter.ToPredicate<Person>(); |
| 131 | +// Safe parsing |
| 132 | +if (Filter.TryParse("invalid query", out Filter? filter)) |
| 133 | +{ |
| 134 | + // Success |
| 135 | + string result = filter.ToMicrosoftGraphFilter(); |
| 136 | +} |
| 137 | +else |
| 138 | +{ |
| 139 | + // Handle parse error |
| 140 | + Console.WriteLine("Invalid query syntax"); |
| 141 | +} |
95 | 142 | ``` |
96 | 143 |
|
97 | 144 | ## Sponsors |
|
0 commit comments