{"id":44959988,"url":"https://github.com/1dev-rs/pagin8","last_synced_at":"2026-03-02T13:18:42.200Z","repository":{"id":288222118,"uuid":"965148743","full_name":"1dev-rs/pagin8","owner":"1dev-rs","description":"C# library that provides a simple way to generate SQL queries via custom DSL","archived":false,"fork":false,"pushed_at":"2026-02-18T15:39:53.000Z","size":245,"stargazers_count":1,"open_issues_count":4,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-02-18T15:45:38.944Z","etag":null,"topics":["csharp","database","dotnet","query","sql"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/1dev-rs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-12T14:18:49.000Z","updated_at":"2026-02-18T15:40:01.000Z","dependencies_parsed_at":"2025-10-03T10:13:42.690Z","dependency_job_id":"fd96badd-7d09-411c-8134-859c4d674fa4","html_url":"https://github.com/1dev-rs/pagin8","commit_stats":null,"previous_names":["1dev-rs/pagin8"],"tags_count":43,"template":false,"template_full_name":null,"purl":"pkg:github/1dev-rs/pagin8","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1dev-rs%2Fpagin8","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1dev-rs%2Fpagin8/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1dev-rs%2Fpagin8/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1dev-rs%2Fpagin8/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/1dev-rs","download_url":"https://codeload.github.com/1dev-rs/pagin8/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1dev-rs%2Fpagin8/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30001652,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-02T11:09:27.951Z","status":"ssl_error","status_checked_at":"2026-03-02T11:08:53.255Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","database","dotnet","query","sql"],"created_at":"2026-02-18T13:02:28.931Z","updated_at":"2026-03-02T13:18:42.191Z","avatar_url":"https://github.com/1dev-rs.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pagin8\n\n**PostgREST-inspired filtering \u0026 pagination for .NET + PostgreSQL**\n\nPagin8 is a C# library that provides a simple and powerful way to build SQL queries with filtering, sorting, and pagination. The URL-based filtering syntax is inspired by PostgREST, making it intuitive and expressive for building RESTful APIs.\n\n## ✨ Features\n\n- 🎯 **PostgREST-inspired syntax** - Intuitive URL-based filtering\n- 🚀 **Zero-boilerplate** - 5 lines per repository, 1 line DI setup\n- 🔍 **Rich operators** - 30+ operators for comparison, strings, arrays, dates\n- 📊 **Smart pagination** - Cursor-based with sorting and count\n- 🎨 **Column selection** - Return only fields you need\n- ⚡ **High performance** - Built on Dapper + PostgreSQL\n- 🏗️ **Clean code** - Repository pattern included\n\n## 🎯 Quick Links\n\n- [Installation](#-installation) - Get started in 2 minutes\n- [Quick Start](#-quick-start---backend-integration) - Working API in 5 steps\n- [Query Examples](#querying-syntax) - Real-world filtering\n- [All Operators](#supported-operators) - Complete reference\n- [Pagination](#paging-operator-for-pagination) - Cursor-based paging\n\n---\n\n## Querying Syntax\n\nTo filter the results of a GET request, add a filter query parameter to the URL, followed by a string that defines the filtering conditions. The basic syntax for a filter is:\n\n```\n\u003ccolumn\u003e=\u003coperator\u003e.\u003cvalue\u003e\n```\nand inside grouping operators:\n```\n\u003ccolumn\u003e.\u003coperator\u003e.\u003cvalue\u003e\n```\n\nwhere `\u003ccolumn\u003e` is the name of the column you want to filter on, `\u003coperator\u003e` is one of the supported operators, and `\u003cvalue\u003e` is the value you want to compare against.\n\nTo filter the data on specific endpoint, use URL query parameters. For example, to retrieve all customers with the name `John`, you can make a GET request to:\n\n```\nhttp://\u003ccustomer_filter_endpoint\u003e?name=eq.John\n```\n\n## Complex Expressions\n\nYou can construct complex filtering expressions by combining multiple conditions using logical operators (and, or, not). For example, to filter the orders table by both the customer_name and order_date columns, you would use the following URL:\n\n```\nGET /\u003corders_filter_endpoint\u003e?customer_name=eq.John\u0026order_date=gt.2022-01-01\n```\n\nThis would return all the orders where the customer_name is equal to `John` and the order_date is after `January 1st, 2022`.\n\nYou can also go further and apply more complex logic to the conditions:\n\n```\nGET /\u003cstudent_filtering_endpoint\u003e?grade=gte.6\u0026student=is.true\u0026or=(age.eq.20,not.and(age.lte.17,age.gte.19))\n```\n\nMultiple conditions on columns are evaluated using `AND` by default, but you can combine them using `OR` with the or operator. For example, query above will return students where:\n\n* The student's `grade` is greater than or equal to 6 (grade \u003e= 6)\n* The student `status` is active (student = true)\n* Either:\n  * The student's `age` is equal to 20 (age = 20)\n  * Or, the student's `age` is not less then or equal to 17 and greater then or equal to 19 (so 18)\n\n\n## Supported Operators\n\nPagin8 support a wide range of operators for comparing values and constructing complex expressions. Here are the supported operators:\n\n### Comparison Operators\n\n-   `eq`: Equal to\n-   `gt`: Greater than\n-   `lt`: Less than\n-   `gte`: Greater than or equal to\n-   `lte`: Less than or equal to\n\n### String Operators\n\n-   `like`: Like\n-   `cs`: Contains\n-   `stw`: Starts With\n-   `enw`: Ends With\n-   `not.`: Logical NOT prefix for all operators ( `not.like` | `not.cs`... )\n\n### Logical Operators\n\n-   `and`: Logical AND\n-   `or`: Logical OR\n-   `not`: Logical NOT ( `not.and()` | `not.or()` )\n\n### Array Operators\n\n-   `in`: In\n-   `\u003ccomparison_operator\u003e.in`: Applies comparison to multiple values at once\n-   `not`: Logical NOT ( `not.in` )\n-   `incl`: Includes all values\n-   `excl`: Exclude all values\n\n### Boolean Operators\n\n-   `is`: Is (true | false), ($empty, not.$empty)\n-   `not`: Logical NOT ( `not.is` )\n\n### Date Range Operators\n\n-   `ago`\n-   `for`\n-   `not`: Logical NOT ( `not.ago` | `not.for` )\n\n\n## Vertical Filtering (Columns)\n\nIn some cases, certain tables in a dataset may contain wide columns with a large amount of data. To optimize server performance and improve response times, Pagin8 offers the ability to withhold these columns from the API response. The client can specify which columns are required using the `select` parameter. If select is not provided, default is `*`, meaning all columns will be returned. `*` token should not be provided in select as it is default one.\n\n```\nGET /\u003cstudent_filtering_endpoint\u003e?select=name,age\n```\n```json\n[\n  {\"name\": \"John\", \"age\": 20},\n  {\"name\": \"Jane\", \"age\": 23}\n]\n```\n\n\n## Nested Filtering Syntax\n\nThe `with` operator allows nested filtering based on complex types within the filtering context.\n\n### Syntax\n\nThe syntax for using the `with` operator is as follows:\n\n```\n\u003ccomplexField\u003e.with=(\u003cconditions\u003e)\n```\n\n-   `\u003ccomplexField\u003e`: Specifies the complex type field that needs to be unfolded for filtering.\n-   `\u003cconditions\u003e`: Specifies the filtering condition for the nested properties.\n\n### Indicator for Complex Type\n\nThe presence of `with` indicates that the filtered property is a complex type containing nested properties.\n\nOne more indicator is column metadata which is returned after `metaInclude=columns` is used in a filter, in the following format:\n\n```json\n...\n{\n          \"name\": \"userTags\",\n          \"type\": \"object\",\n          \"flags\": [\n            \"no-sort\",\n            \"array\"\n          ],\n          \"properties\": [\n            {\n              \"name\": \"colorCode\",\n              \"type\": \"string\"\n            },\n            {\n              \"name\": \"description\",\n              \"type\": \"string\"\n            },\n            {\n              \"name\": \"extraData\",\n              \"type\": \"string\"\n            },\n            {\n              \"name\": \"id\",\n              \"type\": \"int\"\n            },\n            {\n              \"name\": \"name\",\n              \"type\": \"string\"\n            }\n          ]\n        },\n        {\n          \"name\": \"versionId\",\n          \"type\": \"int\"\n        }\n        ...\n```\nIn the case of complex type, `properties` information is included in the response, so the client is aware of inner operations applicable to nested properties.\n\nThe `type` attribute specifies whether the field is a primitive type like an integer or a complex object. The presence of `array` in `flags` signifies that the field is an array of objects, providing detailed information through nested properties. Omitting `array` in flags and keeping `type: object`, will indicate that it is just a single nested object.\n\nExample:\n  - `type:object` + `flags: [array]` = **array of complex objects** (`GetTagInfo[]` in case of `userTags`)\n  - `type:object` + `flags: []` = **single complex object** (`Contact` i.e.`)\n\n\n\n### Unwrapping and Inner Conditions\n\nWhen `with` is used, it unwraps the complex type (`userTags`) and applies the specified conditions to filter its inner properties.\n\n### Supported Operators\n\nCurrently, only two operators are supported for nested filtering:\n\n-   `incl`: Includes values that match all of the provided values.\n-   `excl`: Excludes values that match any of the provided values.\n\n### Usage Examples\n\n#### Including and Excluding Values\n\nTo filter `userTags` based on included and excluded values:\n\n`userTags.with=(name.incl(test1,test2),name.excl(test3,test4))`\n\nThis filters `userTags` to include values where `name` is both `test1` and `test2`, excluding values where `name` is either `test3` or `test4`.\n\nNote that if you want to filter out some special character, like `#`, it must be decoded and sent like `%23`:\n\n`userTags.with=(colorCode.incl(%23FF0000))`\n\n### Future Enhancements\n\nCurrently, only the `incl` and `excl` operators are supported for nested filtering. However,  expanding the capabilities of the filtering system to include support for all other operators is in progress.\n\n## Date Range Operator\n\nThe Date Range allows you to specify a range of time based on the current date. It is used with the operators `ago` and `for`. The `ago` operator calculates the range starting from the current moment and going backward, while the `for` operator calculates the range starting from the current moment and going forward.\n\nTo indicate an exact range, you can use the following units:\n\n-   `d` for days\n-   `w` for weeks\n-   `m` for months\n-   `y` for years\n\nThe calculation includes the first moment of the previous day, week, month, or year (for `ago`), or the last moment of the specified day, week, month, or year (for `for`). However, when the `e` modifier is added, like `de`, `we`, `me`, and `ye` the calculation starts from the current moment plus the specified number of units (calculates exact period from the current date).\n\n| Operator | Description                             | Example               |\n|----------|-----------------------------------------|-----------------------|\n| ago.1d    | Range from start of previous day until now                     | createdDate=ago.1d     |\n| ago.1w    | Range from start of previous week until now                    | createdDate=ago.1w     |\n| ago.1m    | Range from start of previous month until now                   | createdDate=ago.1m     |\n| ago.1y    | Range from start of previous year until now                    | createdDate=ago.1y     |\n| ago.1de   | Range of one day ago (exact)             | createdDate=ago.1de    |\n| ago.1we   | Range of one week ago (exact)            | createdDate=ago.1we    |\n| ago.1me   | Range of one month ago (exact)           | createdDate=ago.1me    |\n| ago.1ye   | Range of one year ago (exact)            | createdDate=ago.1ye    |\n| for.1d    | Range from now until end of the next day                 | createdDate=for.1d     |\n| for.1w    | Range from now until end of the next week                | createdDate=for.1w     |\n| for.1m    | Range from now until end of the next month               | createdDate=for.1m     |\n| for.1y    | Range from now until end of the next year                | createdDate=for.1y     |\n| for.1de   | Range of one day forward (exact)         | createdDate=for.1de    |\n| for.1we   | Range of one week forward (exact)        | createdDate=for.1we    |\n| for.1me   | Range of one month forward (exact)       | createdDate=for.1me    |\n| for.1ye   | Range of one year forward (exact)        | createdDate=for.1ye    |\n\n\n## Paging Operator for Pagination\n\nThe custom paging operator allows you to perform paginated queries by specifying sorting criteria, result limit, and count settings.\n\n### Syntax\n\nThe syntax for the custom paging operator in URL form is as follows:\n\n`paging=(sort(\u003csort_criteria\u003e),limit.\u003climit_value\u003e,count.\u003ccount_value\u003e)`\n\n-   `\u003csort_criteria\u003e`: Specify the sorting criteria for the query. The sorting criteria should be provided in the format `field_name.direction.$lastValue`, where:\n\n    -   `field_name`: The name of the field to sort by.\n    -   `direction`: The sorting direction, either `asc` for ascending or `desc` for descending.\n    -   `$lastValue`: The last known value of the field from the previous page (only after initial request).\n-   `\u003climit_value\u003e`: The maximum number of results to retrieve per page.\n\n-   `\u003ccount_flag\u003e`: Specify whether to include the total count of matching records in the response. Use `true` to include the count or `false` to exclude it.\n\n### Initial paging\n\nTo initialize the first page by certain criteria, it is enough that you put just columns with directions in `sort` criteria, in the order you would like to sort, with maximum number of rows you want to fetch with `limit`. Also, you need to privide `count` flag, depending on your need to see the count of filtered data or not:\n\n`GET /\u003cstudent_filtering_endpoint\u003e?paging=(sort(name.asc,address.desc),limit.10,count.true)`\n\n-   `sort(name.asc,address.desc)`: Sorting by the `name` field in ascending order, then by `address` in descending order.\n\n-   `limit.10`: Limiting the results to a maximum of 10 per page.\n\n-   `count.true`:  Including the total count of matching records from the response.\n\n\n### Paging after initial request\n\nTo initialize the every next page by same criteria, besides columns with directions in sort criteria, you need to provide also last known values, together with primary key last value. Also, you can set the `count` flag to false, if you want to omit count this time:\n\n`GET /\u003cstudent_filtering_endpoint\u003e?paging=(sort(name.asc.John,address.desc.New York,$key.45),limit.10,count.false)`\n\n-   `name.asc.John`: Sorting by the `name` field in ascending order. `John`  represents the last known value of the `name` field from the previous page.\n\n-   `address.desc.New York`: Second level sorting is in descending order by the `address` field. `New York`  represents the last known value of the `address` field from the previous page.\n\n-   `$key.45`: Key is a placeholder for a primary key from that table you are querying, and you got it from the metadata. `45` represents the last known value of the primary key from the previous page. Note that you do not need to replace `$key` placeholder with real field name as the library will resolve it for you.\n\n-   `limit.10`: Limiting the results to a maximum of 10 per page.\n\n-   `count.false`:  Excluding the total count of matching records from the response.\n\nThere are a few more placeholders that you can use while setting the last value, those are `$empty` for empty strings, and `$null` for `null` values.\n\n## Retrieve Count Only with `paging(count.true)`\n\nTo optimize your queries, you can use the `paging` operator to retrieve only the count of the data without returning the actual data rows.\n\n### Example Usage\n\nTo retrieve the count of data without fetching the data rows, make a GET request to the desired resource endpoint and include only `paging(count.true)` in the query parameter:\n\n`GET /\u003cendpoint\u003e?paging=(count.true)`\n\n### Response\n\nThe response to the above request will include the count of the data without the actual data rows. This allows you to efficiently retrieve the count without incurring the overhead of fetching and transmitting the entire dataset, enhancing the performance of your queries.\n\n## Operator summary table\n\n| Operator        | Type             | Description                                                    | Example Non-nested Usage                    | Example Nested Usage (inside `and()`, `or()`)         |\n|-----------------|------------------|----------------------------------------------------------------|---------------------------------------------|-------------------------------------------------------|\n| eq              | Comparison       | Equal to                                                       | `name=eq.John`                              | `name.eq.John`                                        |\n| gt              | Comparison       | Greater than                                                   | `age=gt.30`                                 | `age.gt.30`                                           |\n| lt              | Comparison       | Less than                                                      | `salary=lt.50000`                           | `salary.lt.50000`                                     |\n| gte             | Comparison       | Greater than or equal to                                       | `score=gte.80`                              | `score.gte.80`                                        |\n| lte             | Comparison       | Less than or equal to                                          | `count=lte.10`                              | `count.lte.10`                                        |\n| like            | String           | Like                                                           | `name=like.Joh%`                            | `name.like.Joh%`                                      |\n| cs              | String           | Contains                                                       | `text=cs.apple`                             | `text.cs.apple`                                       |\n| stw             | String           | Starts With                                                    | `address=stw.5th`                           | `address.stw.5th`                                     |\n| enw             | String           | Ends With                                                      | `email=enw.com`                             | `email.enw.com`                                       |\n| not.*           | String           | Logical NOT prefix for all string operators                    | `email=not.like.gmail`                      | `email.not.like.gmail`                                |\n| and             | Logical          | Logical AND                                                    | `and=(age.gte.18,state.eq.NY)`              | `and(age.gte.18,state.eq.NY)`                         |\n| or              | Logical          | Logical OR                                                     | `or=(grade=eq.A,grade=eq.B)`                | `or(grade=eq.A,grade=eq.B)`                           |\n| not             | Logical          | Logical NOT                                                    | `not.and=(grade.gte.6,grade.lte.8)`         | `not.and(grade.gte.6,grade.lte.8)`                    |\n| in              | Array            | In                                                             | `category=in.(1,2,3)`                       | `category.in.(1,2,3)`                                 |\n| not.in          | Array            | Logical NOT for the `in` operator                              | `category=not.in.(1,2,3)`                   | `category.not.in.(1,2,3)`                             |\n| eq.in           | Comparison+Array | Equal to any value(default, same like in)                      | `role=eq.in.(Admin,User)`                   | `and=(role.eq.in.(Admin,User),status.eq.active)`      |\n| not.eq.in       | Comparison+Array | Not equal to any value(not.in)                                 | `type=not.eq.in.(A,B)`                      | `or=(type.not.eq.in.(A,B),type.eq.C)`                 |\n| stw.in          | Comparison+Array | Starts with any value                                          | `name=stw.in.(Adm,Man)`                     | `and=(name.stw.in.(Adm,Man),active.eq.true)`          |\n| not.stw.in      | Comparison+Array | Does not start with any value                                  | `name=not.stw.in.(Test,Temp)`               | `or=(name.not.stw.in.(Test,Temp),status.eq.closed)`   |\n| enw.in          | Comparison+Array | Ends with any value                                            | `file=enw.in.(.pdf,.doc)`                   | `and=(file.enw.in.(.pdf,.doc),archived.eq.false)`     |\n| like.in         | Comparison+Array | Matches any pattern (wildcards supported)                      | `email=like.in.(%@gm%,%@yah%)`              | `or=(email.like.in.(%@gm%,%@yah%),user.eq.John)`      |\n| is              | Boolean          | Is (true or false)                                             | `active=is.true`                            | `active.is.true`                                      |\n| is.not          | Boolean          | Logical NOT for the `is` operator                              | `active=not.is.true`                        | `active.not.is.true`                                  |\n| is              | Any              | Is $empty - none                                               | `name=is.$empty`                            | `name.is.$empty`                                      |\n| is.not          | Any              | Is not $empty - any                                            | `name=is.not.$empty`                        | `name.is.not.$empty`                                  |\n| ago             | Date Range       | Specifies a range from start of previous period until now      | `createdDate=ago.1w`                        | `createdDate.ago.1w`                                  |\n| for             | Date Range       | Specifies a range from now until the end of the next period    | `createdDate=for.1y`                        | `createdDate.for.1y`                                  |\n| ago(exact)      | Date Range       | Specifies a range of time ago from the current date            | `createdDate=ago.1we`                       | `createdDate.ago.1we`                                 |\n| for(exact)      | Date Range       | Specifies a range of time forward from the current date        | `createdDate=for.1ye`                       | `createdDate.for.1ye`                                 |\n| not.ago         | Date Range       | Logical NOT for the `ago` operator                             | `createdDate=not.ago.1w`                    | `createdDate.not.ago.1w`                              |\n| not.for         | Date Range       | Logical NOT for the `for` operator                             | `createdDate=not.for.1m`                    | `createdDate.not.for.1m`                              |\n| incl/excl       | Array            | Includes/excludes all values                                   | `name.incl(tag_a,tag_b), name.excl(tag_c)`  | -                                                     |\n| with            | Nested Filtering | Unfolds nested property                                        | `userTags.with=(\u003cinnerConditions\u003e)`         | `userTags.with.(\u003cinnerConditions\u003e)`                   |\n\n## Metadata\n\nGet additional information about your queries by including `metaInclude` in the URL. This helps build dynamic UIs and track schema changes.\n\n**Available options:** `filters`, `columns`\n\n### Additional Meta Information in the Response\n\n#### Filters Meta\nWhen the `metaInclude=filters` token is present, the response will include the following metadata in additional information in **filtersMeta** key:\n\n- **Data.Filters**: This field provides details about the available filters for the entity. It includes the filter options and their corresponding values.\n\n- **Data.DefaultFilter**: This field indicates the default filter applied to the entity. It contains the filter criteria, such as name, sorting, limit, and count options.\n\n- **Table Key**: The table key indicates the primary key or identifier used for the entity.\n\n- **ActiveFilter**: The active filter indicates the complete filter applied to the results being fetched.\n\n- **Hash**: The hash value represents the current default filter hash for the entity. It is recommended that the client store this value. When the client receives a new hash value, it serves as an indicator that the default filter has been changed. In such cases, the client should send a request with the \"metaInclude=filters\" token to ensure awareness of the changes.\n\nWhen there is no `metaInclude` token, where will be basic metadata in additional info, something like:\n\n```json\n\"additionalInformation\": {\n    \"filtersMeta\": {\n      \"activeFilter\": \"status=eq.active\u0026paging=(sort(createdAt.desc,id.asc),limit.50,count.false)\",\n      \"tableKey\": \"id\",\n      \"hash\": \"A1B2C3D4\"\n    },\n    \"columnMeta\": {\n      \"hash\": \"E5F6G7H8\"\n    }\n  }\n```\n\nWhen `metaInclude=filters` token is provided, a full filter metadata response is returned:\n\n```json\n{\n  \"additionalInformation\": {\n    \"filtersMeta\": {\n      \"data\": {\n        \"filters\": [\n\t\t\t{\n\t          \"id\": 1,\n\t          \"viewCode\": \"OJ\",\n\t          \"name\": \"Name filter\",\n\t          \"schema\": \"name=stw.tr\",\n\t          \"accessLevel\": 2,\n\t          \"setBy\": \"1\",\n\t          \"setDateTime\": \"2023-06-30T10:30:47.724671\"\n\t        },...\n\t\t],\n        \"defaultFilter\": {\n          \"id\": 0,\n          \"viewCode\": \"OJ\",\n          \"name\": \"\",\n          \"schema\": \"paging=(sort(modifiedDate.desc,id.asc))\",\n          \"accessLevel\": 3,\n          \"setBy\": \"1\",\n          \"setDateTime\": \"2023-06-30T10:30:47.724671\"\n        }\n      },\n      \"hash\": \"F87348D7\",\n      \"tableKey\": \"id\"\n    }\n  }\n}\n```\n\n#### Columns meta\n\nWhen the `metaInclude=columns` token is present, the response will include the following metadata in additional information in **columnsMeta** key:\n\n- **Data**: This field provides array of objects which contains details about the available columns for entity - name and data type.\n\n- **Hash**: The hash value represents the current columns hash for the entity. It is recommended that the client store this value. When the client receives a new hash value, it serves as an indicator that the colums have been changed. In such cases, the client should send a request with the \"metaInclude=columns\" token to ensure awareness of the changes.\n\nWhen `metaInclude=columns` token is provided, a full column metadata response is returned:\n\n```json\n\"additionalInformation\": {\n    \"filtersMeta\": {\n      \"activeFilter\": \"metaInclude=columns\",\n      \"tableKey\": \"id\",\n      \"hash\": \"A1B2C3D4\"\n    },\n    \"columnMeta\": {\n      \"data\": [\n        { \"name\": \"id\", \"type\": \"int\" },\n        { \"name\": \"name\", \"type\": \"string\" },\n        { \"name\": \"email\", \"type\": \"string\" },\n        { \"name\": \"status\", \"type\": \"string\" },\n        { \"name\": \"createdAt\", \"type\": \"DateTime\" },\n        { \"name\": \"price\", \"type\": \"decimal\" }\n      ],\n      \"hash\": \"E5F6G7H8\"\n    }\n  }\n```\n\n---\n\n**💡 Use Case:** Build smart frontends that cache metadata and only refresh when hash changes.\n\n\n## 📦 Installation\n\n### NuGet Packages\n\n```bash\n# Core library (query building, filtering syntax)\ndotnet add package 1Dev.Pagin8\n\n# Backend extensions (ASP.NET Core + Dapper + PostgreSQL)\ndotnet add package 1Dev.Pagin8.Extensions.Backend\n```\n\n---\n\n## 🚀 Quick Start - Backend Integration\n\n### 1️⃣ Setup (Program.cs)\n\n```csharp\nusing _1Dev.Pagin8.Extensions.Backend.Extensions;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add Pagin8 core\nbuilder.Services.AddPagin8(config =\u003e\n{\n    config.DatabaseType = DatabaseType.PostgreSql;\n});\n\n// Add Backend Extensions (ONE line setup!)\nbuilder.Services.AddPagin8Backend(\n    builder.Configuration.GetConnectionString(\"DefaultConnection\")!\n);\n\nvar app = builder.Build();\napp.Run();\n```\n\n### 2️⃣ Create Repository (5 lines!)\n\n```csharp\nusing _1Dev.Pagin8.Extensions.Backend.Base;\nusing _1Dev.Pagin8.Extensions.Backend.Interfaces;\n\npublic class ProductRepository : FilteredRepositoryBase\u003cProductDto\u003e\n{\n    protected override string ViewName =\u003e \"vw_products\";\n    protected override string? DefaultFilter =\u003e \"isDeleted=eq.false\";\n\n    public ProductRepository(IFilterProvider filterProvider)\n        : base(filterProvider) { }\n\n    // ✅ GetFilteredAsync() is inherited - zero boilerplate!\n}\n```\n\n### 3️⃣ Create Service\n\n```csharp\nusing _1Dev.Pagin8.Extensions.Backend.Models;\n\npublic class ProductService\n{\n    private readonly IProductRepository _repository;\n\n    public async Task\u003cPagedResults\u003cProductDto\u003e\u003e GetFilteredAsync(FilteredDataQuery query)\n    {\n        return await _repository.GetFilteredAsync(query);\n    }\n}\n```\n\n### 4️⃣ Create Controller\n\n```csharp\nusing Microsoft.AspNetCore.Mvc;\nusing _1Dev.Pagin8.Extensions.Backend.Extensions;\n\n[ApiController]\n[Route(\"api/[controller]\")]\npublic class ProductsController : ControllerBase\n{\n    private readonly IProductService _service;\n\n    [HttpGet]\n    public async Task\u003cIActionResult\u003e Get()\n    {\n        var query = HttpContext.ToFilteredDataQuery(); // ✨ Extension method!\n        var result = await _service.GetFilteredAsync(query);\n\n        return Ok(new\n        {\n            data = result.Data,\n            totalRows = result.TotalRows\n        });\n    }\n}\n```\n\n### 5️⃣ Test Your API\n\n```bash\n# Basic filtering\nGET /api/products?name=cs.laptop\u0026price=gte.500\n\n# With sorting and pagination\nGET /api/products?category=eq.electronics\u0026paging=(sort(price.asc),limit.20,count.true)\n\n# Complex queries\nGET /api/products?and=(price.gte.100,status.eq.active)\u0026createdAt=ago.30d\n```\n\n**Response:**\n```json\n{\n  \"data\": [\n    { \"id\": 1, \"name\": \"Laptop Pro\", \"price\": 1299.99, \"category\": \"electronics\" },\n    { \"id\": 2, \"name\": \"Laptop Air\", \"price\": 999.99, \"category\": \"electronics\" }\n  ],\n  \"totalRows\": 42\n}\n```\n\n---\n\n## 🎯 What You Get\n\n| Component | What It Does | Code Required |\n|-----------|-------------|---------------|\n| **DI Setup** | Registers all services (connection, query builder, filter provider) | **1 line** |\n| **Repository** | Full CRUD + filtering per entity | **5 lines** |\n| **Controller** | HTTP → Query conversion | **Extension method** |\n| **Infrastructure** | Connection pooling, Dapper integration, query building | **0 lines (in NuGet)** |\n| **Maintenance** | Library updates, bug fixes, new features | **NuGet update** |\n\n### Real Impact\n- ✅ **5 minutes** from install to working API\n- ✅ **Type-safe** - No raw SQL strings\n- ✅ **Testable** - Mock `IFilterProvider` for unit tests\n- ✅ **Production-ready** - Connection pooling, prepared statements\n- ✅ **Extensible** - Override base methods, add custom logic\n\n---\n\n## 📚 Backend Extensions - Components\n\n### Models\n\n#### `PagedResults\u003cT\u003e`\nGeneric paged results model returned from queries.\n\n```csharp\npublic record PagedResults\u003cT\u003e\n{\n    public IEnumerable\u003cT\u003e Data { get; init; }\n    public int TotalRows { get; init; }\n    public Meta? Meta { get; set; } // Pagin8 metadata\n}\n```\n\n#### `FilteredDataQuery`\nQuery parameters for filtered data requests.\n\n```csharp\npublic record FilteredDataQuery\n{\n    public string QueryString { get; init; }\n    public string DefaultQuery { get; init; }\n    public bool IgnoreLimit { get; init; }\n\n    public static FilteredDataQuery Create(string? queryString, bool ignoreLimit = false);\n}\n```\n\n### Base Classes\n\n#### `FilteredRepositoryBase\u003cTResponse\u003e`\nInherit from this class to get filtering support automatically.\n\n```csharp\npublic abstract class FilteredRepositoryBase\u003cTResponse\u003e : IFilteredRepository\u003cTResponse\u003e\n{\n    protected abstract string ViewName { get; }\n    protected virtual string? DefaultFilter =\u003e null;\n\n    // ✅ These methods are inherited automatically:\n    Task\u003cPagedResults\u003cTResponse\u003e\u003e GetFilteredAsync(FilteredDataQuery query);\n    Task\u003cint\u003e GetFilteredCountAsync(FilteredDataQuery query);\n}\n```\n\n### Extension Methods\n\n#### `HttpContext.ToFilteredDataQuery()`\nConverts HTTP request query string to `FilteredDataQuery`.\n\n```csharp\n[HttpGet]\npublic async Task\u003cIActionResult\u003e Get()\n{\n    var query = HttpContext.ToFilteredDataQuery();\n    // or with default filter\n    var query = HttpContext.ToFilteredDataQuery(\"status=eq.active\");\n}\n```\n\n### Advanced Usage\n\n#### Custom Connection Factory\n\n```csharp\npublic class MyConnectionFactory : IDbConnectionFactory\n{\n    public IDbConnection Create()\n    {\n        // Your custom connection logic\n        return new NpgsqlConnection(connectionString);\n    }\n}\n\n// Register it\nbuilder.Services.AddPagin8Backend\u003cMyConnectionFactory\u003e();\n```\n\n#### Multiple Default Filters\n\n```csharp\npublic class ProductRepository : FilteredRepositoryBase\u003cProductDto\u003e\n{\n    protected override string ViewName =\u003e \"vw_products\";\n    protected override string? DefaultFilter =\u003e \"and=(isDeleted.eq.false,isActive.eq.true)\";\n}\n```\n\n#### Working with Views\n\n```csharp\n// Create a PostgreSQL view\nCREATE VIEW vw_products AS\nSELECT\n    p.id,\n    p.name,\n    p.price,\n    c.name as category_name,\n    p.created_at\nFROM products p\nLEFT JOIN categories c ON p.category_id = c.id;\n\n// Use it in your repository\npublic class ProductRepository : FilteredRepositoryBase\u003cProductDto\u003e\n{\n    protected override string ViewName =\u003e \"vw_products\";\n}\n```\n\n---\n\n## 🔍 Diagnostics \u0026 Query Logging\n\nPagin8 supports built-in query logging through the standard `Microsoft.Extensions.Logging` infrastructure. When enabled, it outputs the generated SQL, parameter values with types, the source entity, and the original filter string — all in a single structured log entry.\n\nNo additional configuration is needed inside Pagin8 — logging is controlled entirely through your application's logging configuration.\n\n### Serilog\n\n```json\n{\n  \"Serilog\": {\n    \"MinimumLevel\": {\n      \"Default\": \"Information\",\n      \"Override\": {\n        \"Pagin8\": \"Verbose\"\n      }\n    }\n  }\n}\n```\n\n### Standard .NET Logging\n\n```json\n{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Pagin8\": \"Trace\"\n    }\n  }\n}\n```\n\n### Output\n\n```\ntrce: Pagin8[1001]\n      Query built for ProductDto | Filter: \"name=cs.laptop\u0026price=gte.500\"\n      | SQL: AND name ILIKE @p0 ESCAPE '\\' AND price \u003e= @p1 ORDER BY id ASC LIMIT @p2\n      | Params (3): [@p0 (String) = 'laptop', @p1 (Decimal) = '500', @p2 (Int32) = '1000000']\n```\n\n### Notes\n\n- Logging uses `LogLevel.Trace` (Serilog: `Verbose`) — the most granular level. It will not appear unless explicitly enabled for the `Pagin8` category.\n- When the log level is not enabled, there is **negligible performance overhead** — a single `IsEnabled` check exits early before any string formatting or allocations occur. When logging *is* active, the `LoggerMessage` source generator is used internally to avoid boxing and unnecessary allocations in the logging pipeline.\n- Parameter values are included in the output. Since this is the most verbose log level, this is by design — but be aware that parameters may contain **user-provided data** (names, emails, etc.). Do not enable `Trace`/`Verbose` level in production environments where logs are stored long-term or exposed to unauthorized parties.\n\n---\n\n## 🔧 Extending the Library\n\nPagin8 is designed to be extended for your specific needs. Here are common extension patterns:\n\n### Adding Custom Metadata\n\nYou can extend the metadata system to include project-specific information:\n\n```csharp\n// 1. Extend PagedResults\npublic record CustomPagedResults\u003cT\u003e : PagedResults\u003cT\u003e\n{\n    public MyCustomMeta? CustomMetadata { get; set; }\n}\n\n// 2. Create custom repository base\npublic abstract class MyRepositoryBase\u003cT\u003e : FilteredRepositoryBase\u003cT\u003e\n    where T : class\n{\n    public override async Task\u003cPagedResults\u003cT\u003e\u003e GetFilteredAsync(FilteredDataQuery query)\n    {\n        var result = await base.GetFilteredAsync(query);\n\n        // Add your custom logic\n        if (result is CustomPagedResults\u003cT\u003e custom)\n        {\n            custom.CustomMetadata = await LoadCustomMetadata();\n        }\n\n        return result;\n    }\n\n    protected abstract Task\u003cMyCustomMeta\u003e LoadCustomMetadata();\n}\n```\n\n### Adding Repository-Level Logic\n\nOverride base methods to add caching, logging, or business rules:\n\n```csharp\npublic class ProductRepository : FilteredRepositoryBase\u003cProductDto\u003e\n{\n    private readonly IMemoryCache _cache;\n    private readonly ILogger\u003cProductRepository\u003e _logger;\n\n    protected override string ViewName =\u003e \"vw_products\";\n\n    public override async Task\u003cPagedResults\u003cProductDto\u003e\u003e GetFilteredAsync(FilteredDataQuery query)\n    {\n        _logger.LogInformation(\"Filtering products with query: {Query}\", query.QueryString);\n\n        // Add caching\n        var cacheKey = $\"products_{query.QueryString}\";\n        if (_cache.TryGetValue(cacheKey, out PagedResults\u003cProductDto\u003e cached))\n            return cached;\n\n        var result = await base.GetFilteredAsync(query);\n        _cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));\n\n        return result;\n    }\n}\n```\n\n### Custom Query Transformations\n\nIntercept and modify queries before execution:\n\n```csharp\npublic class TenantAwareRepository : FilteredRepositoryBase\u003cMyDto\u003e\n{\n    private readonly ITenantContext _tenantContext;\n\n    protected override string ViewName =\u003e \"my_table\";\n\n    public override async Task\u003cPagedResults\u003cMyDto\u003e\u003e GetFilteredAsync(FilteredDataQuery query)\n    {\n        // Automatically add tenant filter\n        var tenantId = _tenantContext.CurrentTenantId;\n        var modifiedQuery = query with\n        {\n            DefaultQuery = $\"tenantId=eq.{tenantId}\"\n        };\n\n        return await base.GetFilteredAsync(modifiedQuery);\n    }\n}\n```\n\n### Adding Global Query Interceptors\n\nRegister middleware to intercept all queries:\n\n```csharp\npublic class QueryAuditInterceptor : IFilterProvider\n{\n    private readonly IFilterProvider _inner;\n    private readonly IAuditService _audit;\n\n    public QueryAuditInterceptor(FilterProvider inner, IAuditService audit)\n    {\n        _inner = inner;\n        _audit = audit;\n    }\n\n    public async Task\u003cPagedResults\u003cTResponse\u003e\u003e GetAsync\u003cTResponse\u003e(\n        string viewName,\n        FilteredDataQuery query) where TResponse : class\n    {\n        await _audit.LogQuery(viewName, query.QueryString);\n        return await _inner.GetAsync\u003cTResponse\u003e(viewName, query);\n    }\n}\n\n// Register it\nbuilder.Services.Decorate\u003cIFilterProvider, QueryAuditInterceptor\u003e();\n```\n\n---\n\n## 📄 License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\n---\n\n## 🤝 Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n---\n\n## 📮 Support\n\n- **Issues:** [GitHub Issues](https://github.com/1dev-rs/pagin8/issues)\n- **Discussions:** [GitHub Discussions](https://github.com/1dev-rs/pagin8/discussions)\n\n---\n\nMade with ❤️ by [1DEV](https://github.com/1dev-rs)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1dev-rs%2Fpagin8","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F1dev-rs%2Fpagin8","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1dev-rs%2Fpagin8/lists"}